Some Questions about BiometricPrompt on Android - android

I'm trying to figure out how to modify (if it's possible) the normal behavior of biometricPrompt, in particular i want to display Gandalf, when the authentication fails.
I'm currently displaying it with a custom alertDialog, but it remains in background, with the biometricPrompt fragment on foreground exactly like this, and it loses all of its dumbness...
The best solution would probably be to display both, alertDialog and biometricPrompt, on foreground, displaying the image only in the upper half of the screen, but at the moment I've no idea of how to do it, or better, I have no idea how to link layouts together to manage size / margins and everything else.
The other thing I was thinking is to remove the biometricPrompt, so the alert dialog will be put on foreground, but any solution I've tried has failed miserably.
Any type of help/ideas will be welcome.
Anyway, here's the code:
class BiometricPromptManager(private val activity: FragmentActivity) {
private val cryptoManager = CryptoManager(activity)
fun authenticateAndDecrypt(failedAction: () -> Unit, successAction: (String) -> Unit) {
// display biometric prompt, if the user is authenticated, the decryption will start
// if biometric related decryption gives positives results, the successAction will start services data decryption
val executor = Executors.newSingleThreadExecutor()
val biometricPrompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
cryptoManager.startDecrypt(failedAction,successAction)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
activity.runOnUiThread { failedAction() }
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
activity.runOnUiThread { failedAction() }
}
})
val promptInfo = biometricPromptInfo()
biometricPrompt.authenticate(promptInfo)
}
private fun biometricPromptInfo(): BiometricPrompt.PromptInfo {
return BiometricPrompt.PromptInfo.Builder()
.setTitle("Fingerprint Authenticator")
.setNegativeButtonText(activity.getString(android.R.string.cancel))
.build()
}
}
Open biometric auth from activity :
private fun openBiometricAuth(){
if(sharedPreferences.getBoolean("fingerPrintEnabled",false)) {
if (BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) { // check for hardware/permission
biometric.visibility = View.VISIBLE
BiometricPromptManager(this).authenticateAndDecrypt(::failure, ::callDecryption)
}
}
}
What to do when the user is not recognized :
private fun failure(){
val view = layoutInflater.inflate(R.layout.gandalf, null)
val builder = AlertDialog.Builder(this)
builder.setView(view)
builder.setPositiveButton("Dismiss") { dialog: DialogInterface, id: Int -> dialog.cancel() }
val alertDialog = builder.create()
alertDialog.show()
}

The Biometric API itself handles failed attempts at authentication the following way:
For each failed attempt the onAuthenticationFailed() callback is invoked.
The user gets 5 tries, after the 5th attempt fails the onAuthenticationError() callback receives the error code ERROR_LOCKOUT and the user must wait for 30 seconds to try again.
Showing your dialog inside onAuthenticationFailed() may be too eager and may result in poor user experience. And so a good place might be after you get the ERROR_LOCKOUT. The way the AndroidX Biometric Library works is that it dismisses the BiometricPrompt when the error is sent. Therefore you should have little problem showing your own dialog at that point.
In any case -- i.e. beyond these opinions -- the more general approach is to call cancelAuthentication() to dismiss the prompt and then proceed to showing your own dialog that way.
Also please follow blogpost1 and blogpost2 for recommended design pattern for BiometricPrompt.

Related

When flow collect stop itself?

There is ParentFragment that shows DialogFragment. I collect a dialog result through SharedFlow. When result received, dialog dismissed. Should I stop collect by additional code? What happens when dialog closed, but fragment still resumed?
// ParentFragment
private fun save() {
val dialog = ContinueDialogFragment(R.string.dialog_is_save_task)
dialog.show(parentFragmentManager, "is_save_dialog")
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}
}
class ContinueDialogFragment(
#StringRes private val titleStringId: Int,
#StringRes private val messageStringId: Int? = null
) : DialogFragment() {
private val _resultSharedFlow = MutableSharedFlow<Int>(1)
val resultSharedFlow = _resultSharedFlow.asSharedFlow()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let { context ->
AlertDialog.Builder(context)
.setTitle(getString(titleStringId))
.setMessage(messageStringId?.let { getString(it) })
.setPositiveButton(getString(R.string.dialog_yes)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_YES)
}
.setNegativeButton(getString(R.string.dialog_no)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_NO)
}
.setNeutralButton(getString(R.string.dialog_continue)) { _, _ ->
_resultSharedFlow.tryEmit(RESULT_CONTINUE)
}
.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
companion object {
const val RESULT_YES = 1
const val RESULT_NO = 0
const val RESULT_CONTINUE = 2
}
}
When a Flow completes depends on its original source. A Flow built with flowOf or asFlow() ends once it reaches the last item in its list. A Flow built with the flow builder could be finite or infinite, depending on whether it has an infinite loop in it.
A flow created with MutableSharedFlow is always infinite. It stays open until the coroutine collecting it is cancelled. Therefore, you are leaking the dialog fragment with your current code because you are hanging onto its MutableSharedFlow reference, which is capturing the dialog fragment reference. You need to manually cancel your coroutine or collection.
Or more simply, you could use first() instead of collect { }.
Side note, this is a highly unusual uses of a Flow, which is why you're running into this fragile condition in the first place. A Flow is for a series of emitted objects, not for a single object.
It is also very fragile that you're collecting this flow is a function called save(), but you don't appear to be doing anything in save() to store the instance state such that if the activity/fragment is recreated you'll start collecting from the flow again. So, if the screen rotates, the dialog will reappear, the user could click the positive button, and nothing will be saved. It will silently fail.
DialogFragments are pretty clumsy to work with in my opinion. Anyway, I would take the easiest route and directly put your behaviors in the DialogFragment code instead of trying to react to the result back in your parent fragment. But if you don't want to do that, you need to go through the pain of calling back through to the parent fragment. Alternatively, you could use a shared ViewModel between these two fragments that will handle the dialog results.
I believe you will have a memory leak of DialogFragment: ParentFragment will be referencing the field dialog.resultSharedFlow until the corresponding coroutine finishes execution. The latter may never happen while ParentFragment is open because dialog.resultSharedFlow is an infinite Flow. You can call cancel() to finish the coroutine execution and make dialog eligible for garbage collection:
lifecycleScope.launch {
dialog.resultSharedFlow.collect {
when (it) {
ContinueDialogFragment.RESULT_YES -> {
viewModel.saveTask()
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_NO -> {
closeFragment()
cancel()
}
ContinueDialogFragment.RESULT_CONTINUE -> {
// dont close fragment
}
}
}
}

Kotlin Multiplatform Mobile: Ktor - how to cancel active coroutine (network request, background work) in Kotlin Native (iOS)?

In my project I write View and ViewModel natively and share Repository, Db, networking.
When user navigates from one screen to another, I want to cancel all network requests or other heavy background operations that are currently running in the first screen.
Example function in Repository class:
#Throws(Throwable::class)
suspend fun fetchData(): List<String>
In Android's ViewModel I can use viewModelScope to automatically cancel all active coroutines. But how to cancel those tasks in iOS app?
Lets suppose that the object session is a URLSession instance, you can cancel it by:
session.invalidateAndCancel()
I didn't find any first party information about this or any good solution, so I came up with my own. Shortly, it will require turning repository suspend functions to regular functions with return type of custom interface that has cancel() member function. Function will take action lambda as parameter. On implementation side, coroutine will be launched and reference for Job will be kept so later when it is required to stop background work interface cancel() function will cancel job.
In addition, because it is very hard to read type of error (in case it happens) from NSError, I wrapped return data with custom class which will hold error message and type. Earlier I asked related question but got no good answer for my case where ViewModel is written natively in each platform.
If you find any problems with this approach or have any ideas please share.
Custom return data wrapper:
class Result<T>(
val status: Status,
val value: T? = null,
val error: KError? = null
)
enum class Status {
SUCCESS, FAIL
}
data class KError(
val type: ErrorType,
val message: String? = null,
)
enum class ErrorType {
UNAUTHORIZED, CANCELED, OTHER
}
Custom interface
interface Cancelable {
fun cancel()
}
Repository interface:
//Convert this code inside of Repository interface:
#Throws(Throwable::class)
suspend fun fetchData(): List<String>
//To this:
fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable
Repository implementation:
override fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable = runInsideOfCancelableCoroutine {
val result = executeAndHandleExceptions {
val data = networkExample()
// do mapping, db operations, etc.
data
}
action.invoke(result)
}
// example of doing heavy background work
private suspend fun networkExample(): List<String> {
// delay, thread sleep
return listOf("data 1", "data 2", "data 3")
}
// generic function for reuse
private fun runInsideOfCancelableCoroutine(task: suspend () -> Unit): Cancelable {
val job = Job()
CoroutineScope(Dispatchers.Main + job).launch {
ensureActive()
task.invoke()
}
return object : Cancelable {
override fun cancel() {
job.cancel()
}
}
}
// generic function for reuse
private suspend fun <T> executeAndHandleExceptions(action: suspend () -> T?): Result<T> {
return try {
val data = action.invoke()
Result(status = Status.SUCCESS, value = data, error = null)
} catch (t: Throwable) {
Result(status = Status.FAIL, value = null, error = ErrorHandler.getError(t))
}
}
ErrorHandler:
object ErrorHandler {
fun getError(t: Throwable): KError {
when (t) {
is ClientRequestException -> {
try {
when (t.response.status.value) {
401 -> return KError(ErrorType.UNAUTHORIZED)
}
} catch (t: Throwable) {
}
}
is CancellationException -> {
return KError(ErrorType.CANCELED)
}
}
return KError(ErrorType.OTHER, t.stackTraceToString())
}
}
You probably have 3 options:
If you're using a some sort of reactive set up iOS side (e.g. MVVM) you could just choose to ignore cancellation. Cancellation will only save a minimal amount of work.
Wrap your iOS calls to shared code in an iOS reactive framework (e.g. combine) and handle cancellation using the iOS framework. The shared work would still be done, but the view won't be updated as your iOS framework is handling cancellation when leaving the screen.
Use Flow with this closable helper

Handling file download with gRPC on Android

I currently have a gRPC server which is sending chunks of a video file. My android application written in Kotlin uses coroutines for UI updates (on Dispatchers.MAIN) and for handling a unidirectional stream of chunks (on Dispatchers.IO). Like the following:
GlobalScope.launch(Dispatchers.Main) {
viewModel.downloadUpdated().accept(DOWNLOAD_STATE.DOWNLOADING) // MAKE PROGRESS BAR VISIBLE
GlobalScope.launch(Dispatchers.IO) {
stub.downloadVideo(request).forEach {
file.appendBytes(
it.data.toByteArray()
)
}
}.join()
viewModel.downloadUpdated().accept(DOWNLOAD_STATE.FINISHED) // MAKE PROGRESS BAR DISAPPEAR
} catch (exception: Exception) {
viewModel.downloadUpdated().accept(DOWNLOAD_STATE.ERROR) // MAKE PROGRESS BAR DISAPPEAR
screenNavigator.showError(exception) // SHOW DIALOG
}
}
This works pretty well but I wonder if there is not a 'cleaner' way to handle downloads. I already know about DownloadManager but I feel like it only accepts HTTP queries and so I can't use my gRPC stub (I might be wrong, please tell me if so). I also checked WorkManager, and here is the same problem I do not know if this is the proper way of handling that case.
So, there are two questions here:
Is there a way to handle gRPC queries in a clean way, meaning that I can now when it starts, finishes, fails and that I can cancel properly?
If not, is there a better way to use coroutines for that ?
EDIT
For those interested, I believe I came up with a dummy algorithm for downloading while updating the progress bar (open to improvments):
suspend fun downloadVideo(callback: suspend (currentBytesRead: Int) -> Unit) {
println("download")
stub.downloadVideo(request).forEach {
val data = it.data.toByteArray()
file.appendBytes(data)
callback(x) // Where x is the percentage of download
}
println("downloaded")
}
class Fragment : CoroutineScope { //NOTE: The scope is the current Fragment
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job
fun onCancel() {
if (job.isActive) {
job.cancel()
}
}
private suspend fun updateLoadingBar(currentBytesRead: Int) {
println(currentBytesRead)
}
fun onDownload() {
launch(Dispatchers.IO) {
downloadVideo { currentBytes ->
withContext(Dispatchers.Main) {
updateLoadingBar(currentBytes)
if (job.isCancelled)
println("cancelled !")
}
}
}
}
}
For more info, please check: Introduction to coroutines
EDIT 2
As proposed in comments we could actually use Flows to handle this and it would give something like:
suspend fun foo(): Flow<Int> = flow {
println("download")
stub.downloadVideo(request).forEach {
val data = it.data.toByteArray()
file.appendBytes(data)
emit(x) // Where x is the percentage of download
}
println("downloaded")
}
class Fragment : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job
fun onCancel() {
if (job.isActive) {
job.cancel()
}
}
private suspend fun updateLoadingBar(currentBytesRead: Int) {
println(currentBytesRead)
}
fun onDownload() {
launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
foo()
.onCompletion { cause -> println("Flow completed with $cause") }
.catch { e -> println("Caught $e") }
.collect { current ->
if (job.isCancelled)
return#collect
updateLoadingBar(current)
}
}
}
}
}
gRPC can be many things so in that respect your question is unclear. Most importantly, it can be fully async and callback-based, which would mean it can be turned into a Flow that you can collect on the main thread. File writing, however, is blocking.
Your code seems to send the FINISHED signal right away, as soon as it has launched the download in the background. You should probably replace launch(IO) with withContext(IO).

How to set a fallback method if fingerprint does not work

I recently moved my project to AndroidX and while implementing fingerprint for the app I am using the Biometric for AndroidX.
implementation 'androidx.biometric:biometric:1.0.0-alpha03'
When a dialog is displayed to use fingerprint for authentication, the dialog has "Cancel" option set as the negative button.
final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Log into App")
.setSubtitle("Please touch the fingerprint sensor to log you in")
.setDescription("Touch Sensor")
.setNegativeButtonText("Cancel".toUpperCase())
.build();
As per the android documentation:
https://developer.android.com/reference/androidx/biometric/BiometricPrompt.PromptInfo.Builder.html#setNegativeButtonText(java.lang.CharSequence)
Required: Set the text for the negative button.
This would typically be used as a "Cancel" button, but may be also used
to show an alternative method for authentication,
such as screen that asks for a backup password.
So instead of "Cancel" button I can say "Use Password" to provide an alternative method incase fingerprint fails, and when user clicks on it I can show another popup dialog where I can let user enter the device password to help retrieve the app password from the Keystore. Is this correct ?
But, what happens if I do not have password set to unlock my phone instead I use a pattern ?
I see that if I use android.hardware.biometrics.BiometricPrompt.Builder instead of androidx.biometric.BiometricPrompt.PromptInfo.Builder, it has a method https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.Builder.html#setDeviceCredentialAllowed(boolean)
for the same purpose, to let user authenticate using other means if fingerprint fails.
Can someone help me understand this ? How I could achieve this with AndroidX as my app is compatible from API 16 onwards. And why does AndroidX does not come back with this fallback method ?
The setDeviceCredentialAllowed API was recently added in beta01
See the release notes here
https://developer.android.com/jetpack/androidx/releases/biometric
On SDK version Q and above using BiometricPrompt with authentication callback otherwise using createConfirmDeviceCredentialsIntent.
val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val biometricPrompt = BiometricPrompt.Builder(this)
.setTitle(getString(R.string.screen_lock_title))
.setDescription(getString(R.string.screen_lock_desc))
.setDeviceCredentialAllowed(true)
.build()
val cancellationSignal = CancellationSignal()
cancellationSignal.setOnCancelListener {
println("#Biometric cancellationSignal.setOnCancelListener")
//handle cancellation
}
val executors = mainExecutor
val authCallBack = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
super.onAuthenticationError(errorCode, errString)
print("SecuritySetupActivity.onAuthenticationError ")
println("#Biometric errorCode = [${errorCode}], errString = [${errString}]")
//handle authentication error
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
super.onAuthenticationSucceeded(result)
print("SecuritySetupActivity.onAuthenticationSucceeded ")
println("#Biometric result = [${result}]")
//handle authentication success
}
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) {
super.onAuthenticationHelp(helpCode, helpString)
print("SecuritySetupActivity.onAuthenticationHelp ")
println("#Biometric helpCode = [${helpCode}], helpString = [${helpString}]")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
print("SecuritySetupActivity.onAuthenticationFailed ")
//handle authentication failed
}
}
biometricPrompt.authenticate(cancellationSignal, executors, authCallBack)
} else {
val i = km.createConfirmDeviceCredentialIntent(getString(R.string.screen_lock_title), getString(R.string.screen_lock_desc))
startActivityForResult(i, 100)
}
Try setDeviceCredentialAllowed(true) on BiometricPromopt.
androidx 1.0.0 allows you to setup a fallback with ease - like this:
// Allows user to authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric login for my app")
.setSubtitle("Log in using your biometric credential")
// Can't call setNegativeButtonText() and
// setAllowedAuthenticators(... or DEVICE_CREDENTIAL) at the same time.
// .setNegativeButtonText("Use account password")
.setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
.build()
see this

Convert Listener to Single in RxJava2

I am using the Play Services Auth api Phone and so far I have the foll
fun startSmsListener() {
val client = SmsRetriever.getClient(applicationContext /* context */);
val task = client.startSmsRetriever();
task.addOnSuccessListener(object : OnSuccessListener<Void> {
override fun onSuccess(p0: Void?) {
//do somethin
}
})
task.addOnFailureListener(object : OnFailureListener {
override fun onFailure(p0: Exception) {
//Handle error
}
})
}
Now I want to put this in an SmsManager class and convert it into an Single/Observable so I can handle it in a reactive way in my viewmodel. How can I do that?
So far I've got this:
var single = Single.create(SingleOnSubscribe<Void> { e ->
val task = client.startSmsRetriever()
task.addOnSuccessListener {
e.onSuccess(it)
}
task.addOnFailureListener {
e.onError(it)
}
})
But I am unsure as to whether this code is correct or not, whether there is something im missing like removing the listeners after disposal.
Any help?
You are interested in a "boolean" value - either connected or not connected, thus instead of Single you should use Completable:
Completable.create { emitter ->
val client = SmsRetriever.getClient(applicationContext)
val task = client.startSmsRetriever()
task.addOnSuccessListener { emitter.onComplete() }
task.addOnFailureListener { emitter.tryOnError(it) }
}
While creating a Completable manually will work, you might also have a look at the RxTask project. It provides "RxJava 2 binding for Google Play Services Task APIs".
If you need it just in one place, an extra library would certainly be an overkill. But if you plan to use more Play Services together with RxJava, it might be worth a look...
It doesn't (yet) provide a wrapper explicitly for SmsRetriever, but the general task helper classes would probably be enough:
val client = SmsRetriever.getClient(applicationContext)
val smsReceiver = CompletableTask.create(client::startSmsRetriever)

Categories

Resources