I am using BiometricPrompt to let the user use fingerprint authentication to log into the app
I have done the following in my PasswordActivity class:
Executor executor = Executors.newSingleThreadExecutor();
FragmentActivity activity = this;
final BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor, new BiometricPrompt.AuthenticationCallback() {
#Override
public void onAuthenticationError(int errorCode, #NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
// user clicked negative button
} else {
//TODO: Called when an unrecoverable error has been encountered and the operation is complete.
}
}
#Override
public void onAuthenticationSucceeded(#NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
//TODO: Called when a biometric is recognized.
final String decryptedText = decryptText();
runOnUiThread(new Runnable() {
#Override
public void run() {
if (decryptedText != null && !decryptedText.isEmpty()) {
editPassword.setText(decryptedText);
buttonNext();
}
}
});
}
#Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
//TODO: Called when a biometric is valid but not recognized.
}
});
final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("My App"))
.setSubtitle("Log on into the app"))
.setNegativeButtonText("Cancel").toUpperCase())
.build();
if (sharedPreferenceManager.isFingerprintEnabled(this))
biometricPrompt.authenticate(promptInfo);
This is the exception that I am getting. Do I have to set?
setNegativeButton (CharSequence text,
Executor executor,
DialogInterface.OnClickListener listener) as well?
I am using implementation 'androidx.biometric:biometric:1.0.0-alpha03' this version.
Caused by java.lang.IllegalArgumentException: Executor must not be null
at android.hardware.biometrics.BiometricPrompt$Builder.setNegativeButton + 182(BiometricPrompt.java:182)
at androidx.biometric.BiometricFragment.onCreate + 201(BiometricFragment.java:201)
at androidx.fragment.app.Fragment.performCreate + 2414(Fragment.java:2414)
at androidx.fragment.app.FragmentManagerImpl.moveToState + 1418(FragmentManagerImpl.java:1418)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState + 1784(FragmentManagerImpl.java:1784)
at androidx.fragment.app.FragmentManagerImpl.moveToState + 1861(FragmentManagerImpl.java:1861)
at androidx.fragment.app.FragmentManagerImpl.dispatchStateChange + 3269(FragmentManagerImpl.java:3269)
at androidx.fragment.app.FragmentManagerImpl.dispatchCreate + 3223(FragmentManagerImpl.java:3223)
at androidx.fragment.app.FragmentController.dispatchCreate + 190(FragmentController.java:190)
at androidx.fragment.app.FragmentActivity.onCreate + 369(FragmentActivity.java:369)
at androidx.appcompat.app.AppCompatActivity.onCreate + 85(AppCompatActivity.java:85)
Can you try replacing Executor executor = Executors.newSingleThreadExecutor(); with:
private Handler handler = new Handler();
private Executor executor = new Executor() {
#Override
public void execute(Runnable command) {
handler.post(command);
}
};
This is according to the code given in this developer.android.com tutorial.
Try to update the dependency, the currently latest version is already a release candidate:
implementation "androidx.biometric:biometric:1.0.0-rc01"
Caused by java.lang.IllegalArgumentException: Executor must not be null
at android.hardware.biometrics.BiometricPrompt$Builder.setNegativeButton + 182(BiometricPrompt.java:182)
This indicates that the framework on the device you're testing is either not receiving the executor from the support library (bug in support library), or the framework itself has a bug.
Could you try on a later version of the androidx.biometric library? Beta02 was recently released, a lot of things have been fixed since alpha03.
Also, what device are you testing, if it's reproducible on Beta02 could you grab a bugreport via adb bugreport foo.zip and attach your sample app with the bug to the public issue tracker?
Related
To display sensible data users can enable authentication in my app. I am using the android in-build authentication.
However, if the user did not secure his device using any pattern, pin, password or biometric authentication, I would like to open the android settings, where he can setup his authentication. Is there any Intent/ way to go there? I did not find it.
Some code so far:
To determine, if the user did not setup any authentication method:
androidx.biometric.BiometricPrompt biometricPrompt = new BiometricPrompt((FragmentActivity) activity, executor, new BiometricPrompt.AuthenticationCallback() {
#Override
public void onAuthenticationError(int errorCode, #NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
// Determine, if the user has no device password set.
boolean errorCodeIsBeingHandledSeparately = false;
// HERE WE DETERMINE THAT CREDENTIALS HAVE NOT BEEN SETUP
if (errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL) {
if (authenticationInterface != null) {
errorCodeIsBeingHandledSeparately = true;
authenticationInterface.onUserHasNoDevicePassWordSet();
}
}
// Display error message, only if the error code is not being handled seperately.
if (!errorCodeIsBeingHandledSeparately) {
Toast.makeText(activity, "Authentication error\n" + errString, Toast.LENGTH_LONG).show();
}
}
#Override
public void onAuthenticationSucceeded(#NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
if (authenticationInterface == null) {
Toast.makeText(activity, "Success", Toast.LENGTH_LONG).show();
}
else {
authenticationInterface.onUserSuccessfullyAuthenticated();
}
}
#Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
Toast.makeText(activity, "Authentication failed", Toast.LENGTH_LONG).show();
}
});
The interface to receive authentication return.
/**
* Interface to receive authentication return.
*/
private AuthenticationUtils.AuthenticationInterface authenticationInterface;
public interface AuthenticationInterface {
public void onUserSuccessfullyAuthenticated();
public void onUserHasNoDevicePassWordSet();
}
The dialog where I want to lead the user to go to the device setup credentials.
public void displayNoDeviceCredentialsSetDialog() {
MaterialAlertDialogBuilder noDeviceCredentialsDialog = new MaterialAlertDialogBuilder(activity, R.style.AlertDialogTheme);
String noDeviceCredentials_goToSettings_dialogMessage = activity.getString(R.string.authentication_noDeviceCredentials_goToSettings_dialogMessage);
noDeviceCredentialsDialog.setMessage(noDeviceCredentials_goToSettings_dialogMessage);
noDeviceCredentialsDialog.setPositiveButton(
R.string.DialogConfirmationOK,
new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
// HERE I WOULD LIKE TO OPEN THE ANDROID SETTING WHERE HE CAN SETUP HIS CREDENTIALS
}
}
);
noDeviceCredentialsDialog.setNegativeButton(
R.string.DialogConfirmationNegativeAnswerText,
null
);
noDeviceCredentialsDialog.show();
}
I would like to go here:
You can get there from settings here:
What i am looking for is something like this: Here we navigate the user to some other android settings.
Intent intent2 = new Intent();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent2.setAction(android.provider.Settings.ACTION_APPLICATION_SETTINGS);
intent2.putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, getPackageName());
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
intent2.setAction(android.provider.Settings.ACTION_APPLICATION_SETTINGS);
intent2.putExtra("app_package", getPackageName());
intent2.putExtra("app_uid", getApplicationInfo().uid);
} else {
intent2.setAction(android.provider.Settings.ACTION_APPLICATION_SETTINGS);
intent2.addCategory(Intent.CATEGORY_DEFAULT);
intent2.setData(Uri.parse("package:" + getPackageName()));
}
startActivity(intent2);
Just figured it out.
There are 3 viable options to use: Settings.ACTION_BIOMETRIC_ENROLL, Settings.ACTION_FINGERPRINT_ENROLL and Settings.ACTION_SECURITY_SETTINGS.
Final implementation I use is:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
activity.startActivity(new Intent(Settings.ACTION_BIOMETRIC_ENROLL));
}
else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
activity.startActivity(new Intent(Settings.ACTION_FINGERPRINT_ENROLL));
}
else {
activity.startActivity(new Intent(Settings.ACTION_SECURITY_SETTINGS));
}
}
Settings.ACTION_FINGERPRINT_ENROLL opens this: After chosing backup lock screen method and setup the chosen method, the device will ask you to register a fingerprint.
Settings.ACTION_SECURITY_SETTINGS opens this:
In lack of a device higher than Android Build "R" I could not test ACTION_BIOMETRIC_ENROLL, but I presume it will be similar to ACTION_FINGERPRINT_ENROLL.
If you want to see what options there are to open android settings. You can just use "CTRL" + "mouse click" on any Settings.XXX (ACTION_SECURITY_SETTINGS, ACTION_FINGERPRINT_ENROLL, ...) in Android Studio.
You will then see "..\android\platforms\android-31\android.jar!\android\provider\Settings.class"
In case you struggle to figure out which API version is described with "Build.VERSION_CODES.P" you can also click "CTRL" + "Mose Click" on the Build version (P, O, ...).
You will then see this:
we implemented authentication (and signing) using the androidx support library. It works perfect on Device with Android 10 (also the DeviceCredentialUnlock) and with Biometrics (the code is little different, as we want to know if it was signed with biometrics or with device credentials).
With Android 8 and older it never calls the callback. No error message is logged, the CompletableFuture is never completed.
CompletableFuture<String> signingResult = new CompletableFuture<>();
final String title = fragmentActivity.getString(R.string.LockScreenTitle);
final String description = fragmentActivity.getString(R.string.LockScreenDescription);
final String data = "testData";
Executor executor = ContextCompat.getMainExecutor(fragmentActivity);
final BiometricPrompt.AuthenticationCallback callback = new BiometricPrompt.AuthenticationCallback() {
#Override
public void onAuthenticationError(int errorCode, #NonNull CharSequence errString) {
LOGGER.info("onAuthenticationError");
Exception exception = mapAuthenticationError(errorCode, errString);
signingResult.completeExceptionally(exception);
}
#Override
public void onAuthenticationSucceeded(#NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
try {
LOGGER.info("onAuthenticationSucceeded");
String signedData = SignInternal(data);
signingResult.complete(signedData);
} catch (SecureTokenException e) {
signingResult.completeExceptionally(e);
}
}
#Override
public void onAuthenticationFailed() {
LOGGER.info("onAuthenticationFailed");
signingResult.completeExceptionally(new SecureTokenException(SecureTokenException.ErrorCode.ABORTED_BY_USER));
}
};
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setDescription(description)
.setDeviceCredentialAllowed(true)
.build();
final BiometricPrompt bp = new BiometricPrompt(fragmentActivity, executor, callback);
fragmentActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
bp.authenticate(promptInfo);
}
});
try {
String signed = signingResult.get();
The keys are generated with the .setUserAuthenticationValidityDurationSeconds(5); property. the sign Method uses the authenticated for signing. But the callback is never called, so we do not come to this point (at Android 9...).
Thank you for your help!
I've recently implemented the androidx biometric lib in my app. First of all the BiometricPrompt.PromptInfo.Builder.setDeviceCredentialAllowed() is deprecated and we should now use the BiometricPrompt.PromptInfo.Builder.setAllowedAuthenticators(int) instead. The int parameter passed to that function must be one of or a combination of the enum values in BiometricManager.Authenticators:
BIOMETRIC_STRONG
BIOMETRIC_WEAK
DEVICE_CREDENTIAL
This Android Developer docs describes the different enums:
https://developer.android.com/reference/androidx/biometric/BiometricManager.Authenticators
From Android Developer docs on Biometric Authentication (I recommend reading this guide btw):
Note: The following combinations of authenticator types aren't supported on Android 10 (API level 29) and lower: DEVICE_CREDENTIAL and BIOMETRIC_STRONG | DEVICE_CREDENTIAL. To check for the presence of a PIN, pattern, or password on Android 10 and lower, use the KeyguardManager.isDeviceSecure() method.
Below is a small block of code that I use to setup my biometric prompt, hopefully this and the information above will guide you in the right direction.
val bioPromptInfoBuilder = BiometricPrompt.PromptInfo.Builder()
.setTitle(authPromptParams.promptTitle)
.setSubtitle(authPromptParams.promptSubtitle)
when {
Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> {
bioPromptInfoBuilder.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
}
kgm.isDeviceSecure -> {
bioPromptInfoBuilder.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_WEAK or
BiometricManager.Authenticators.DEVICE_CREDENTIAL)
}
else -> return null
}
Basically Android 10 and below supports different combinations of the BiometricManager.Authenticators enums.
I'm writing an app that authenticates the user using the native Android Fingerprint API (on Android 6.0 and up).
In one scenario - the device receives a Gcm notification and if the screen is off but the phone is not locked - the app "wakes" the device by launching an activity with the following flags:
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
The app then displays a dialogs that asks the user to authenticate using his finger. In this case - no callback function (from FingerprintManager.AuthenticationCallback - ) is called
here is the code :
fingerprintManager.authenticate(null, cancellationSignal, 0, new FingerprintManager.AuthenticationCallback() {
#Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
logger.info("Authentication error " + errorCode + " " + errString);
...
}
#Override
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
super.onAuthenticationHelp(helpCode, helpString);
logger.info("Authentication help message thrown " + helpCode + " " + helpString);
...
}
#Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
logger.info("Authentication succeeded");
...
}
/*
* Called when authentication failed but the user can try again
* When called four times - on the next fail onAuthenticationError(FINGERPRINT_ERROR_LOCKOUT)
* will be called
*/
#Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
logger.info("Authentication failed");
...
}
}, null);
The same code runs when the screen is on and when it's off but when it's off and turned on by the activity - the callbacks don't get called.
Any ideas?
Thanks in advance!
I've noticed the same issue and in the adb logcat I've seen the next line:
W/FingerprintManager: authentication already canceled
I've searched in depth into source code and I've found the following function in FingerprintManager:
if (cancel != null) {
if (cancel.isCanceled()) {
Log.w(TAG, "authentication already canceled");
return;
} else {
cancel.setOnCancelListener(new OnAuthenticationCancelListener(crypto));
}
}
This means, that you are entering your authenticate() function with already cancelled cancellationSignal. Just add the following before your authenticate():
if(cancellationSignal.isCanceled()){
cancellationSignal = new CancellationSignal();
}
This way you will always pass the non-cancelled cancellationSignal and your flow will be correct.
I have seen plenty of examples of how to use Android TextToSpeak in an Activity, and have also managed to get this to work just fine. I've also managed to get it to work using a bound service in a plugin, but it seems overcomplicated for my purposes. Here is my VoiceService class:
public class VoiceService : IVoiceService, TextToSpeech.IOnInitListener
{
public event EventHandler FinishedSpeakingEventHandler;
private TextToSpeech _tts;
public void Init()
{
// Use a speech progress listener so we get notified when the service finishes speaking the prompt
var progressListener = new SpeechProgressListener();
progressListener.FinishedSpeakingEventHandler += OnUtteranceCompleted;
//_tts = new TextToSpeech(Application.Context, this);
_tts = new TextToSpeech(Mvx.Resolve<IMvxAndroidCurrentTopActivity>().Activity, this);
_tts.SetOnUtteranceProgressListener(progressListener);
}
public void OnInit(OperationResult status)
{
// THIS EVENT NEVER FIRES!
Console.WriteLine("VoiceService TextToSpeech Initialised. Status: " + status);
if (status == OperationResult.Success)
{
}
}
public void Speak(string prompt)
{
if (!string.IsNullOrEmpty(prompt))
{
var map = new Dictionary<string, string> { { TextToSpeech.Engine.KeyParamUtteranceId, new Guid().ToString() } };
_tts.Speak(prompt, QueueMode.Flush, map);
Console.WriteLine("tts_Speak: " + prompt);
}
else
{
Console.WriteLine("tts_Speak: PROMPT IS NULL OR EMPTY!");
}
}
/// <summary>
/// When we finish speaking, call the event handler
/// </summary>
public void OnUtteranceCompleted(object sender, EventArgs e)
{
if (FinishedSpeakingEventHandler != null)
{
FinishedSpeakingEventHandler(this, new EventArgs());
}
}
public void Dispose()
{
//throw new NotImplementedException();
}
public IntPtr Handle { get; private set; }
}
Note that the OnInit method never gets called.
In my viewmodel I'd like to do this:
_voiceService.Init();
_voiceService.FinishedSpeakingEventHandler += _voiceService_FinishedSpeakingEventHandler;
... and then later ...
_voiceService.Speak(prompt);
When I do this I get these messages in the output:
10-13 08:13:59.734 I/TextToSpeech( 2298): Sucessfully bound to com.google.android.tts
(happens when I create the new TTS object)
and
10-13 08:14:43.924 W/TextToSpeech( 2298): speak failed: not bound to TTS engine
(when I call tts.Speak(prompt))
If I was using an activity I would create an intent to get this to work, but I'm unsure how to do that in a plugin.
Thanks in advance,
David
Don't implement Handle yourself, instead derive from Java.Lang.Object
public class VoiceService : Java.Lang.Object, IVoiceService, TextToSpeech.IOnInitListener
and remove your Dispose() and Handle implementation
More info here: http://developer.xamarin.com/guides/android/advanced_topics/java_integration_overview/android_callable_wrappers/
Also, I suggest you take an async approach when implementing your service, which would make calling it from view-model something like
await MvxResolve<ITextToSpeechService>().SpeakAsync(text);
crash log:
java.lang.SecurityException: No permission to modify given thread
android.os.Process.setThreadPriority(Native Method)
android.webkit.WebViewCore$WebCoreThread$1.handleMessage(WebViewCore.java:764)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:137)
android.webkit.WebViewCore$WebCoreThread.run(WebViewCore.java:829)
java.lang.Thread.run(Thread.java:856)
which permission should I declare?
http://developer.android.com/reference/android/Manifest.permission.html
edit:
I found a similar problem in WebView java.lang.SecurityException: No permission to modify given thread
The Answer say "It's cyanogen's fault."
However, in the thread http://code.google.com/p/cyanogenmod/issues/detail?id=5656&thanks=5656&ts=1341224425,cm menbers seem to deny it's CM's bug
Above all, my question is:
How to fix it from my app?
which permission should I declare?
There is no relevant permission for this AFAIK. Unfortunately, the implementation of setThreadPriority() is in native code, which makes it difficult for me to figure out what is going on.
cm menbers seem to deny it's CM's bug
No, they do not. Nobody has posted evidence that it is a problem in CM9 or higher, which is why they marked the issue as stale. If you are seeing this on CM9 or higher, I suggest that you update the issue. If you are seeing this on standard Android, please create a sample project that can reproduce the error.
How to fix it from my app?
You don't, in all likelihood. You could try running some experiments on your WebView, to see if there is some specific content that triggers this exception, and try to modify or eliminate that content.
I get dozens of crash log caused by this exception from my app (which depends on webview heavily), involved ROM version are 4.0.4 and 4.0.3.
It seems that there is no normal way to fix it, so i tried following hacking approach.
code snipet on 4.0.4:
private static Handler sWebCoreHandler;
// Class for providing Handler creation inside the WebCore thread.
private static class WebCoreThread implements Runnable {
// Message id for initializing a new WebViewCore.
private static final int INITIALIZE = 0;
private static final int REDUCE_PRIORITY = 1;
private static final int RESUME_PRIORITY = 2;
public void run() {
Looper.prepare();
Assert.assertNull(sWebCoreHandler);
synchronized (WebViewCore.class) {
sWebCoreHandler = new Handler() {
#Override
public void handleMessage(Message msg) {
// ...
// Process.setPriority(...)
}
};
// ...
}
// ...
}
}
I think this exception is thrown from sWebCoreHandler.handleMessage(), if we can wrap try/catch on handleMessage(), the problem could be fixed.
Handler class has four members:
final MessageQueue mQueue;
final Looper mLooper;
final Callback mCallback;
IMessenger mMessenger;
mQueue is set as mLooper.mQueue, mCallback is null in sWebCoreHandler, so we just need to set mLooper and mMessenger with values in sWebCoreHandler.
static Handler sProxyHandler = null;
static void tryTweakWebCoreHandler() {
// 4.0.3/4.0.4 rom
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
tweakWebCoreHandle();
}
}
static private void tweakWebCoreHandle() {
if (sProxyHandler != null)
return;
try {
Field f = Class.forName("android.webkit.WebViewCore").getDeclaredField("sWebCoreHandler");
f.setAccessible(true);
Object h = f.get(null);
Object mMessenger = null;
Method m = Handler.class.getDeclaredMethod("getIMessenger", (Class<?>[])null);
m.setAccessible(true);
mMessenger = m.invoke(h, (Object[])null);
sProxyHandler = new WebCoreProxyHandler((Handler)h);
if (mMessenger != null) {
Field f1 = Handler.class.getDeclaredField("mMessenger");
f1.setAccessible(true);
f1.set(sProxyHandler, mMessenger);
}
f.set(null, sProxyHandler);
// Log.w(TAG, "sWebCoreHandler: " + h);
} catch (Throwable e) {
Log.w(TAG, "exception: " + e);
}
if (sProxyHandler == null)
sProxyHandler = new Handler();
}
static class WebCoreProxyHandler extends Handler {
final Handler handler;
public WebCoreProxyHandler(Handler handler) {
super(handler.getLooper());
this.handler = handler;
}
public void handleMessage(Message msg) {
// Log.w("WebCoreProxyHandler", "handle msg: " + msg.what);
try {
handler.handleMessage(msg);
} catch (Throwable tr) {
Log.w("WebCoreProxyHandler", "exception: " + tr);
}
}
}
Remain problem is when to invoke tryTweakWebCoreHandler(). I tried to invoke it after a WebView instance is created and tested on some devices, WebCoreProxyHandler.handleMessage() can be called.
Note: i just made some simple test, i'm not sure this problem is resolved as the origin exception can not be reproduced reliably.
If you decide to try this approach, please do enough test.