Android Question [Class Module]Biometrics/KeyGuard Tandem module (Got it working)

Cableguy

Expert
Licensed User
Longtime User
Hi Guys,

I am trying to, using B4XPages, create a KeyguardManager module, and getting partial results...

This is my current code:

B4X:
Sub Class_Globals
    Private mCallback As Object
    Private mEvent As String
    Private RequestCode As Int = 9876
End Sub

Public Sub Initialize(Callback As Object, EventName As String)
    mCallback = Callback
    mEvent = EventName.ToLowerCase
End Sub

Public Sub Show(Title As String, Message As String)
    Dim ctxt As JavaObject
    ctxt.InitializeContext

    Dim km As JavaObject = ctxt.RunMethod("getSystemService", Array("keyguard"))
    Dim intent As Object = km.RunMethod("createConfirmDeviceCredentialIntent", Array(Title, Message))

    If intent <> Null Then
       
        StartActivity(intent)
        ' MUST use JavaObject + GetNativeParent in B4XPages
'        Dim parent As Activity = B4XPages.GetNativeParent(B4XPages.MainPage)
'
'        Dim jo As JavaObject = parent
'        jo.RunMethod("startActivityForResult", Array(intent, RequestCode))
    Else
        CallSubDelayed2(mCallback, mEvent & "_complete", False)
    End If
End Sub

' Called from Main.Activity_Result
Public Sub HandleActivityResult(rc As Int, result As Int)
    If rc = RequestCode Then
        CallSubDelayed2(mCallback, mEvent & "_complete",result)
    End If
End Sub

using StartActivity(Intent) correctly shows the PatternLock screen, but I get no result...
If I use the commented out part Dim parent.... etc, I get an error stating that I am trying to call an intent that that does exist...
java.lang.RuntimeException: Method: startActivityForResult not found in: anywheresoftware.b4a.BALayout

My aim is to create a fallback if FingerPrint is not available/enrolled/notsupported which I have it working using the Biometrics Modeule found in the forum
https://www.b4x.com/android/forum/threads/fingerprint-authentication.72500/#content. I am using the Code Module because it allows me to place meaningfull log statements where I need them.

Any help would be appreciated in getting this module working...
 

Cableguy

Expert
Licensed User
Longtime User
I had found that, and tried to implement it... but then, how to correlate this:

Dim ctxt As JavaObject
ctxt.InitializeContext

B4X:
Dim km As JavaObject = ctxt.RunMethod("getSystemService", Array("keyguard"))
    Dim intent As Object = km.RunMethod("createConfirmDeviceCredentialIntent", Array(Title, Message))

to that example?
 
Upvote 0

Cableguy

Expert
Licensed User
Longtime User
I have been trying to use Copilot to help me, and this is its latest remarks about this subject:

3) So why does Keyguard still fail?

Because Keyguard is special:

❌ It does NOT accept the IOnActivityResult callback object​

✔ It requires the Activity instance itself as the receiver​

This is the difference.

Your ringtone picker example uses:

java

startActivityForResult(IOnActivityResult callback, Intent)

But Keyguard uses the old signature:

java

startActivityForResult(Intent, int)

It does NOT support the callback‑object version.

So your 3‑sub trick cannot be used for Keyguard.


4) This is why the forum solution is required

Erel’s solution works because:

  • The Activity instance (Me in Main) is the receiver
  • The result is delivered through the standard Activity_Result
  • The page receives the result indirectly
This is the only way Keyguard works in B4XPages.


5) So the real problem is NOT the intent

The intent is fine.

The real problem is:

⭐ Keyguard requires the Activity instance​

⭐ Your class does not have access to the Activity instance​

⭐ The IOnActivityResult trick cannot be used​

⭐ Therefore the call must be forwarded to Main​

This is why you keep getting:

Code

Expected receiver of type ComponentActivity,
but got java.lang.Class<b4a.example.main>

Because your class is not the Activity.


6) Final conclusion

Your 3‑sub pattern works for:

  • Ringtone picker
  • File picker
  • Document picker
  • Image picker
  • Any modern Intent that supports the callback‑object signature
But Keyguard does NOT support that signature.

Therefore:

✔ You MUST call startActivityForResult from Main​

✔ You MUST use Me inside Main​

✔ You MUST forward the result from Main to your class​

There is no workaround.
 
Upvote 0

Cableguy

Expert
Licensed User
Longtime User
Based on This example I was able to complete my code module, allowing me to call biometrics authentication, if available, and if not, fallback to keyguard.
Thank you for the input.
If anyone is interested, I can share the code, so that others even improve it.. just drop me a line
 
Upvote 0

Cableguy

Expert
Licensed User
Longtime User
Ok, so here it is...
I have this in a class :

B4X:
'version: 1.10
#Event: Complete (Success As Boolean, ErrorMessage As String)

Sub Class_Globals
    Private ctxt As JavaObject
    Private BioManager As JavaObject
    Private Handler As JavaObject
    Private Executor As Object
    Private mTarget As Object
    Private mEventName As String
    Private ion As Object
    Private xui As XUI
End Sub

Public Sub Initialize(Target As Object, EventName As String)
    mTarget = Target
    mEventName = EventName

    ctxt.InitializeContext
    BioManager = BioManager.InitializeStatic("androidx.biometric.BiometricManager").RunMethod("from", Array(ctxt))

    Handler.InitializeNewInstance("android.os.Handler", Null)
    Executor = Handler.CreateEvent("java.util.concurrent.Executor", "Executor", Null)
End Sub

'====================================================
' EXECUTOR CALLBACK
'====================================================
Private Sub Executor_Event(MethodName As String, Args() As Object) As Object
    If MethodName = "execute" Then
        Handler.RunMethod("post", Array(Args(0)))
    End If
    Return Null
End Sub

'====================================================
' BIOMETRIC CAPABILITY CHECK
'====================================================
Public Sub CanAuthenticate As String
    Dim v As Int = BioManager.RunMethod("canAuthenticate", Null)

    Select v
        Case 0
            Return "SUCCESS"
        Case 1
            Return "UNAVAILABLE"
        Case 11
            Return "NONE_ENROLLED"
        Case 12
            Return "NO_HARDWARE"
        Case Else
            Return "UNKNOWN"
    End Select
End Sub

'====================================================
' SHOW BIOMETRIC PROMPT
'====================================================
Public Sub ShowBiometric(Title As String)
    Dim Builder As JavaObject
    Builder.InitializeNewInstance("androidx.biometric.BiometricPrompt$PromptInfo$Builder", Null)
    Builder.RunMethod("setTitle", Array(Title))
    Builder.RunMethod("setNegativeButtonText", Array("Cancel"))

    Dim Callback As JavaObject
    Callback.InitializeNewInstance(Application.PackageName & ".biometricmanager.BiometricPromptAuthentication", Array(Me))

    Dim Prompt As JavaObject
    Prompt.InitializeNewInstance("androidx.biometric.BiometricPrompt", Array(ctxt, Executor, Callback))
    Prompt.RunMethod("authenticate", Array(Builder.RunMethod("build", Null)))
End Sub

Private Sub Auth_Complete(Success As Boolean, ErrorCode As Int, ErrorMessage As String)
    CallSubDelayed3(mTarget, mEventName & "_Complete", Success, ErrorMessage)
End Sub

'====================================================
' KEYGUARD FALLBACK
'====================================================
Public Sub ShowKeyGuard(Title As String, Description As String)
    Dim km As JavaObject = ctxt.RunMethod("getSystemService", Array("keyguard"))
    Dim intent As Intent = km.RunMethod("createConfirmDeviceCredentialIntent", Array(Title, Description))

    If intent.IsInitialized = False Then
        CallSubDelayed3(mTarget, mEventName & "_Complete", True, "")
        Return
    End If

    StartActivityForResult(intent)
End Sub

Private Sub StartActivityForResult(i As Intent)
    Dim jo As JavaObject = GetBA
    ion = jo.CreateEvent("anywheresoftware.b4a.IOnActivityResult", "ion", Null)
    jo.RunMethod("startActivityForResult", Array As Object(ion, i))
End Sub

Private Sub GetBA As Object
    Dim jo As JavaObject = Me
    Return jo.RunMethod("getBA", Null)
End Sub

Private Sub ion_Event(MethodName As String, Args() As Object) As Object
    Dim Success As Boolean = (Args(0) = -1)
    CallSubDelayed3(mTarget, mEventName & "_Complete", Success, "")
    Return Null
End Sub

#If java
import androidx.biometric.*;

public static class BiometricPromptAuthentication extends BiometricPrompt.AuthenticationCallback {
    private BA ba;
    public BiometricPromptAuthentication(B4AClass parent) {
        ba = parent.getBA();
    }

    @Override
    public void onAuthenticationError(int errorCode, CharSequence errString) {
        super.onAuthenticationError(errorCode, errString);
        ba.raiseEventFromUI(this, "auth_complete", false, errorCode, errString);
    }

    @Override
    public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
        super.onAuthenticationSucceeded(result);
        ba.raiseEventFromUI(this, "auth_complete", true, 0, "");
    }

    @Override
    public void onAuthenticationFailed() {
        super.onAuthenticationFailed();
    }
}
#End If

and in my mainPage:

B4X:
'This event will be called once, before the page becomes visible.
Private Sub B4XPage_Created (Root1 As B4XView)
    Root = Root1
    Root.LoadLayout("1")
    Biometric.Initialize(Me, "Authenticate")

End Sub

Private Sub B4XPage_Disappear
        LastActive = DateTime.Now  
End Sub

Private Sub B4XPage_Appear
    If FirstRun Or DateTime.Now - LastActive > 30000 Then ' lock the vault
        AuthenticateMe
    End If
End Sub

Private Sub AuthenticateMe
    Dim res As String = Biometric.CanAuthenticate

    If res = "SUCCESS" Then
        Biometric.ShowBiometric("Please Authenticate Yourself")
    Else
        Biometric.ShowKeyGuard(" ", "Please Authenticate Yourself")
    End If

    Wait For Authenticate_Complete (Success As Boolean, ErrorMessage As String)

    If Success Then
        Log("Authentication OK")
    Else
        Log("Authentication failed: " & ErrorMessage)
    End If
End Sub

What this does is, test if Biometrics (Fingerprint, faceId, Iris, voice, etc) are available and returns a result in the canAuthenticate. If the result is other than success (the only one we are interested in), it tries to launch Keyguard (PatternLock, Pin, Password, etc). If keyguard is also not available, the user has NOT set any device lock, so we don't enforce it neither...
If, after a successful unlock, the app goes into background for more than 30s, then the user is prompt again for unlock.
I used Copilot to help me tidy up a bit the code module, and I have tested this in a Fingeprint enabled Device as well as a PatternLock only device, and it works fine.
This module was almost just a combination of several sparse code found on the forum.
Any improvements to this module should be shared for everyone benefit.
 
Upvote 0
Top