Merge pull request #573 from PhilKes/fix/biometric-lock-open-note

Fix/biometric lock open note
This commit is contained in:
Phil 2025-05-06 18:41:46 +02:00 committed by GitHub
commit fb35ffdac4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 63 additions and 174 deletions

View file

@ -190,7 +190,7 @@ dependencies {
implementation("androidx.security:security-crypto:1.1.0-alpha06") implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.sqlite:sqlite-ktx:2.4.0") implementation("androidx.sqlite:sqlite-ktx:2.4.0")
implementation("androidx.work:work-runtime:2.9.1") implementation("androidx.work:work-runtime:2.9.1")
implementation("androidx.biometric:biometric:1.1.0")
implementation("cat.ereza:customactivityoncrash:2.4.0") implementation("cat.ereza:customactivityoncrash:2.4.0")
implementation("com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0") implementation("com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0")
implementation("com.github.bumptech.glide:glide:4.15.1") implementation("com.github.bumptech.glide:glide:4.15.1")

View file

@ -64,7 +64,10 @@ abstract class LockedActivity<T : ViewBinding> : AppCompatActivity() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
if (preferences.biometricLock.value == BiometricLock.ENABLED) { if (
preferences.biometricLock.value == BiometricLock.ENABLED &&
notallyXApplication.locked.value
) {
hide() hide()
} }
} }

View file

@ -1,7 +1,6 @@
package com.philkes.notallyx.utils package com.philkes.notallyx.utils
import android.app.Activity import android.app.Activity
import android.app.KeyguardManager
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
@ -12,7 +11,6 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.BiometricManager
import android.net.Uri import android.net.Uri
@ -126,30 +124,15 @@ fun Context.getFileName(uri: Uri): String? =
} }
fun Context.canAuthenticateWithBiometrics(): Int { fun Context.canAuthenticateWithBiometrics(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val biometricManager = androidx.biometric.BiometricManager.from(this)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { val authenticators =
val keyguardManager = ContextCompat.getSystemService(this, KeyguardManager::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val packageManager: PackageManager = this.packageManager androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG or
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
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()
} else { } else {
val biometricManager: BiometricManager = androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
this.getSystemService(BiometricManager::class.java)
return biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
} }
} return biometricManager.canAuthenticate(authenticators)
return BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
} }
fun Context.getUriForFile(file: File): Uri = fun Context.getUriForFile(file: File): Uri =

View file

@ -4,15 +4,13 @@ import android.app.Activity
import android.app.KeyguardManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.Intent 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.Build
import android.os.CancellationSignal
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.philkes.notallyx.R import com.philkes.notallyx.R
import javax.crypto.Cipher import javax.crypto.Cipher
@ -27,7 +25,7 @@ fun Activity.showBiometricOrPinPrompt(
) { ) {
showBiometricOrPinPrompt( showBiometricOrPinPrompt(
isForDecrypt, isForDecrypt,
this, this as FragmentActivity,
activityResultLauncher, activityResultLauncher,
titleResId, titleResId,
descriptionResId, descriptionResId,
@ -48,7 +46,7 @@ fun Fragment.showBiometricOrPinPrompt(
) { ) {
showBiometricOrPinPrompt( showBiometricOrPinPrompt(
isForDecrypt, isForDecrypt,
requireContext(), activity!!,
activityResultLauncher, activityResultLauncher,
titleResId, titleResId,
descriptionResId, descriptionResId,
@ -60,7 +58,7 @@ fun Fragment.showBiometricOrPinPrompt(
private fun showBiometricOrPinPrompt( private fun showBiometricOrPinPrompt(
isForDecrypt: Boolean, isForDecrypt: Boolean,
context: Context, context: FragmentActivity,
activityResultLauncher: ActivityResultLauncher<Intent>, activityResultLauncher: ActivityResultLauncher<Intent>,
titleResId: Int, titleResId: Int,
descriptionResId: Int? = null, descriptionResId: Int? = null,
@ -69,142 +67,55 @@ private fun showBiometricOrPinPrompt(
onFailure: (errorCode: Int?) -> Unit, onFailure: (errorCode: Int?) -> Unit,
) { ) {
when { 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 -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
val fingerprintManager = val promptInfo =
ContextCompat.getSystemService(context, FingerprintManager::class.java) BiometricPrompt.PromptInfo.Builder()
if ( .apply {
fingerprintManager?.isHardwareDetected == true && setTitle(context.getString(titleResId))
fingerprintManager.hasEnrolledFingerprints() descriptionResId?.let {
) { setDescription(context.getString(descriptionResId))
val cipher = }
if (isForDecrypt) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getInitializedCipherForDecryption(iv = cipherIv!!) setAllowedAuthenticators(
} else { BiometricManager.Authenticators.BIOMETRIC_STRONG or
getInitializedCipherForEncryption() 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() { override fun onAuthenticationFailed() {
super.onAuthenticationFailed() super.onAuthenticationFailed()
onFailure.invoke(null) onFailure.invoke(null)
} }
override fun onAuthenticationError( override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
errorCode: Int, super.onAuthenticationError(errorCode, errString)
errString: CharSequence?, onFailure.invoke(errorCode)
) { }
super.onAuthenticationError(errorCode, errString) }
onFailure.invoke(errorCode) val prompt =
} BiometricPrompt(context, ContextCompat.getMainExecutor(context), authCallback)
}, prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
null,
)
} else {
promptPinAuthentication(context, activityResultLauncher, titleResId, onFailure)
}
} }
else -> { 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( private fun promptPinAuthentication(
context: Context, context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>, activityResultLauncher: ActivityResultLauncher<Intent>,