From 2341c3058623f92a26a57672e3724c3cddf7620e Mon Sep 17 00:00:00 2001 From: PhilKes Date: Mon, 5 May 2025 18:30:06 +0200 Subject: [PATCH 1/2] Fix hide on create Activity if unlocked --- .../philkes/notallyx/presentation/activity/LockedActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt b/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt index 68f69ff1..51f45c2c 100644 --- a/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt +++ b/app/src/main/java/com/philkes/notallyx/presentation/activity/LockedActivity.kt @@ -64,7 +64,10 @@ abstract class LockedActivity : AppCompatActivity() { override fun onPause() { super.onPause() - if (preferences.biometricLock.value == BiometricLock.ENABLED) { + if ( + preferences.biometricLock.value == BiometricLock.ENABLED && + notallyXApplication.locked.value + ) { hide() } } From 0fee25f022a4b6d8b37bca8a1c42391bd63bdf7a Mon Sep 17 00:00:00 2001 From: PhilKes Date: Mon, 5 May 2025 18:33:03 +0200 Subject: [PATCH 2/2] Use androidx.biometric to fix compatibility issues --- app/build.gradle.kts | 2 +- .../notallyx/utils/AndroidExtensions.kt | 31 +-- .../notallyx/utils/security/LockUtils.kt | 199 +++++------------- 3 files changed, 59 insertions(+), 173 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2aa7d50e..159c15e7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -190,7 +190,7 @@ dependencies { implementation("androidx.security:security-crypto:1.1.0-alpha06") implementation("androidx.sqlite:sqlite-ktx:2.4.0") implementation("androidx.work:work-runtime:2.9.1") - + implementation("androidx.biometric:biometric:1.1.0") implementation("cat.ereza:customactivityoncrash:2.4.0") implementation("com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0") implementation("com.github.bumptech.glide:glide:4.15.1") diff --git a/app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt b/app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt index c6347e04..391da8fd 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/AndroidExtensions.kt @@ -1,7 +1,6 @@ package com.philkes.notallyx.utils import android.app.Activity -import android.app.KeyguardManager import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -12,7 +11,6 @@ import android.content.ContentResolver import android.content.Context import android.content.ContextWrapper import android.content.Intent -import android.content.pm.PackageManager import android.content.res.Configuration import android.hardware.biometrics.BiometricManager import android.net.Uri @@ -126,30 +124,15 @@ fun Context.getFileName(uri: Uri): String? = } fun Context.canAuthenticateWithBiometrics(): Int { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - val keyguardManager = ContextCompat.getSystemService(this, KeyguardManager::class.java) - val packageManager: PackageManager = this.packageManager - if (!packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE - } - if (keyguardManager?.isKeyguardSecure == false) { - return BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED - } - } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - val biometricManager: BiometricManager = - this.getSystemService(BiometricManager::class.java) - return biometricManager.canAuthenticate() + val biometricManager = androidx.biometric.BiometricManager.from(this) + val authenticators = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG or + androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL } else { - val biometricManager: BiometricManager = - this.getSystemService(BiometricManager::class.java) - return biometricManager.canAuthenticate( - BiometricManager.Authenticators.BIOMETRIC_STRONG or - BiometricManager.Authenticators.DEVICE_CREDENTIAL - ) + androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG } - } - return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE + return biometricManager.canAuthenticate(authenticators) } fun Context.getUriForFile(file: File): Uri = diff --git a/app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt b/app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt index ff14d4fc..fd695f4d 100644 --- a/app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt +++ b/app/src/main/java/com/philkes/notallyx/utils/security/LockUtils.kt @@ -4,15 +4,13 @@ import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.content.Intent -import android.hardware.biometrics.BiometricManager -import android.hardware.biometrics.BiometricPrompt -import android.hardware.fingerprint.FingerprintManager import android.os.Build -import android.os.CancellationSignal -import android.widget.Toast import androidx.activity.result.ActivityResultLauncher +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import com.philkes.notallyx.R import javax.crypto.Cipher @@ -27,7 +25,7 @@ fun Activity.showBiometricOrPinPrompt( ) { showBiometricOrPinPrompt( isForDecrypt, - this, + this as FragmentActivity, activityResultLauncher, titleResId, descriptionResId, @@ -48,7 +46,7 @@ fun Fragment.showBiometricOrPinPrompt( ) { showBiometricOrPinPrompt( isForDecrypt, - requireContext(), + activity!!, activityResultLauncher, titleResId, descriptionResId, @@ -60,7 +58,7 @@ fun Fragment.showBiometricOrPinPrompt( private fun showBiometricOrPinPrompt( isForDecrypt: Boolean, - context: Context, + context: FragmentActivity, activityResultLauncher: ActivityResultLauncher, titleResId: Int, descriptionResId: Int? = null, @@ -69,142 +67,55 @@ private fun showBiometricOrPinPrompt( onFailure: (errorCode: Int?) -> Unit, ) { when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - // Android 11+ with BiometricPrompt and Authenticators - val prompt = - BiometricPrompt.Builder(context) - .apply { - setTitle(context.getString(titleResId)) - descriptionResId?.let { - setDescription(context.getString(descriptionResId)) - } - setAllowedAuthenticators( - BiometricManager.Authenticators.BIOMETRIC_STRONG or - BiometricManager.Authenticators.DEVICE_CREDENTIAL - ) - } - .build() - val cipher = - if (isForDecrypt) { - getInitializedCipherForDecryption(iv = cipherIv!!) - } else { - getInitializedCipherForEncryption() - } - prompt.authenticate( - BiometricPrompt.CryptoObject(cipher), - getCancellationSignal(context), - context.mainExecutor, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult? - ) { - super.onAuthenticationSucceeded(result) - onSuccess.invoke(result!!.cryptoObject!!.cipher) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - onFailure.invoke(null) - } - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { - super.onAuthenticationError(errorCode, errString) - onFailure.invoke(errorCode) - } - }, - ) - } - Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> { - // Android 10: Use BiometricPrompt without Authenticators - val prompt = - BiometricPrompt.Builder(context) - .apply { - setTitle(context.getString(titleResId)) - descriptionResId?.let { - setDescription(context.getString(descriptionResId)) - } - setNegativeButton( - context.getString(R.string.cancel), - context.mainExecutor, - ) { _, _ -> - onFailure.invoke(null) - } - } - .build() - val cipher = - if (isForDecrypt) { - getInitializedCipherForDecryption(iv = cipherIv!!) - } else { - getInitializedCipherForEncryption() - } - prompt.authenticate( - BiometricPrompt.CryptoObject(cipher), - getCancellationSignal(context), - context.mainExecutor, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult? - ) { - super.onAuthenticationSucceeded(result) - onSuccess.invoke(result!!.cryptoObject!!.cipher) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - onFailure.invoke(null) - } - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { - super.onAuthenticationError(errorCode, errString) - onFailure.invoke(errorCode) - } - }, - ) - } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { - val fingerprintManager = - ContextCompat.getSystemService(context, FingerprintManager::class.java) - if ( - fingerprintManager?.isHardwareDetected == true && - fingerprintManager.hasEnrolledFingerprints() - ) { - val cipher = - if (isForDecrypt) { - getInitializedCipherForDecryption(iv = cipherIv!!) - } else { - getInitializedCipherForEncryption() + val promptInfo = + BiometricPrompt.PromptInfo.Builder() + .apply { + setTitle(context.getString(titleResId)) + descriptionResId?.let { + setDescription(context.getString(descriptionResId)) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + setAllowedAuthenticators( + BiometricManager.Authenticators.BIOMETRIC_STRONG or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + } else { + setNegativeButtonText(context.getString(R.string.cancel)) + setAllowedAuthenticators( + BiometricManager.Authenticators.BIOMETRIC_STRONG + ) + } + } + .build() + val cipher = + if (isForDecrypt) { + getInitializedCipherForDecryption(iv = cipherIv!!) + } else { + getInitializedCipherForEncryption() + } + val authCallback = + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult + ) { + super.onAuthenticationSucceeded(result) + onSuccess.invoke(result.cryptoObject!!.cipher!!) } - fingerprintManager.authenticate( - FingerprintManager.CryptoObject(cipher), - getCancellationSignal(context), - 0, - object : FingerprintManager.AuthenticationCallback() { - override fun onAuthenticationSucceeded( - result: FingerprintManager.AuthenticationResult? - ) { - super.onAuthenticationSucceeded(result) - onSuccess.invoke(result!!.cryptoObject!!.cipher) - } - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - onFailure.invoke(null) - } + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + onFailure.invoke(null) + } - override fun onAuthenticationError( - errorCode: Int, - errString: CharSequence?, - ) { - super.onAuthenticationError(errorCode, errString) - onFailure.invoke(errorCode) - } - }, - null, - ) - } else { - promptPinAuthentication(context, activityResultLauncher, titleResId, onFailure) - } + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + onFailure.invoke(errorCode) + } + } + val prompt = + BiometricPrompt(context, ContextCompat.getMainExecutor(context), authCallback) + prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) } else -> { @@ -214,14 +125,6 @@ private fun showBiometricOrPinPrompt( } } -private fun getCancellationSignal(context: Context): CancellationSignal { - return CancellationSignal().apply { - setOnCancelListener { - Toast.makeText(context, R.string.biometrics_failure, Toast.LENGTH_SHORT).show() - } - } -} - private fun promptPinAuthentication( context: Context, activityResultLauncher: ActivityResultLauncher,