Biometric: fixed Android 12+ timing issue

pull/209/head
M66B 2 years ago
parent 2b7b1c83b8
commit 293cd77e55

@ -2643,204 +2643,209 @@ public class Helper {
static void authenticate(final FragmentActivity activity, final LifecycleOwner owner, static void authenticate(final FragmentActivity activity, final LifecycleOwner owner,
Boolean enabled, final Boolean enabled, final
Runnable authenticated, final Runnable cancelled) { Runnable authenticated, final Runnable cancelled) {
Log.i("Authenticate " + activity); // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android12-release/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java#195
ApplicationEx.getMainHandler().post(new RunnableEx("authenticate") {
@Override
public void delegate() {
Log.i("Authenticate " + activity);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
String pin = prefs.getString("pin", null); String pin = prefs.getString("pin", null);
if (enabled != null || TextUtils.isEmpty(pin)) { if (enabled != null || TextUtils.isEmpty(pin)) {
Log.i("Authenticate biometric enabled=" + enabled); Log.i("Authenticate biometric enabled=" + enabled);
BiometricPrompt.PromptInfo.Builder info = new BiometricPrompt.PromptInfo.Builder() BiometricPrompt.PromptInfo.Builder info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(enabled == null ? R.string.app_name : R.string.title_setup_biometrics)); .setTitle(activity.getString(enabled == null ? R.string.app_name : R.string.title_setup_biometrics));
KeyguardManager kgm = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); KeyguardManager kgm = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kgm != null && kgm.isDeviceSecure()) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kgm != null && kgm.isDeviceSecure())
info.setDeviceCredentialAllowed(true); info.setDeviceCredentialAllowed(true);
else else
info.setNegativeButtonText(activity.getString(android.R.string.cancel)); info.setNegativeButtonText(activity.getString(android.R.string.cancel));
info.setConfirmationRequired(false);
info.setSubtitle(activity.getString(enabled == null ? R.string.title_setup_biometrics_unlock info.setConfirmationRequired(false);
: enabled
? R.string.title_setup_biometrics_disable
: R.string.title_setup_biometrics_enable));
final BiometricPrompt prompt = new BiometricPrompt(activity, executor, info.setSubtitle(activity.getString(enabled == null ? R.string.title_setup_biometrics_unlock
new BiometricPrompt.AuthenticationCallback() { : enabled
private int fails = 0; ? R.string.title_setup_biometrics_disable
: R.string.title_setup_biometrics_enable));
@Override final BiometricPrompt prompt = new BiometricPrompt(activity, executor,
public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) { new BiometricPrompt.AuthenticationCallback() {
if (isCancelled(errorCode) || errorCode == BiometricPrompt.ERROR_UNABLE_TO_PROCESS) private int fails = 0;
Log.w("Authenticate biometric error " + errorCode + ": " + errString);
else
Log.e("Authenticate biometric error " + errorCode + ": " + errString);
if (isHardwareFailure(errorCode)) { @Override
prefs.edit().remove("biometrics").apply(); public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) {
ApplicationEx.getMainHandler().post(authenticated); if (isCancelled(errorCode) || errorCode == BiometricPrompt.ERROR_UNABLE_TO_PROCESS)
return; Log.w("Authenticate biometric error " + errorCode + ": " + errString);
} else
Log.e("Authenticate biometric error " + errorCode + ": " + errString);
if (!isCancelled(errorCode)) if (isHardwareFailure(errorCode)) {
ApplicationEx.getMainHandler().post(new RunnableEx("auth:error") { prefs.edit().remove("biometrics").apply();
@Override ApplicationEx.getMainHandler().post(authenticated);
public void delegate() { return;
ToastEx.makeText(activity,
"Error " + errorCode + ": " + errString,
Toast.LENGTH_LONG).show();
} }
});
ApplicationEx.getMainHandler().post(cancelled);
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
Log.i("Authenticate biometric succeeded");
setAuthenticated(activity);
ApplicationEx.getMainHandler().post(authenticated);
}
@Override
public void onAuthenticationFailed() {
Log.w("Authenticate biometric failed");
if (++fails >= 3)
ApplicationEx.getMainHandler().post(cancelled);
}
private boolean isCancelled(int errorCode) {
return (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
errorCode == BiometricPrompt.ERROR_CANCELED ||
errorCode == BiometricPrompt.ERROR_USER_CANCELED);
}
private boolean isHardwareFailure(int errorCode) { if (!isCancelled(errorCode))
return (errorCode == BiometricPrompt.ERROR_HW_UNAVAILABLE || ApplicationEx.getMainHandler().post(new RunnableEx("auth:error") {
errorCode == BiometricPrompt.ERROR_NO_BIOMETRICS || // No fingerprints enrolled. @Override
errorCode == BiometricPrompt.ERROR_HW_NOT_PRESENT || public void delegate() {
errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL); ToastEx.makeText(activity,
} "Error " + errorCode + ": " + errString,
}); Toast.LENGTH_LONG).show();
}
prompt.authenticate(info.build()); });
ApplicationEx.getMainHandler().post(cancelled);
}
final Runnable cancelPrompt = new RunnableEx("auth:cancelprompt") { @Override
@Override public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
public void delegate() { Log.i("Authenticate biometric succeeded");
try { setAuthenticated(activity);
prompt.cancelAuthentication(); ApplicationEx.getMainHandler().post(authenticated);
} catch (Throwable ex) { }
Log.e(ex);
}
}
};
ApplicationEx.getMainHandler().postDelayed(cancelPrompt, 60 * 1000L); @Override
public void onAuthenticationFailed() {
Log.w("Authenticate biometric failed");
if (++fails >= 3)
ApplicationEx.getMainHandler().post(cancelled);
}
owner.getLifecycle().addObserver(new LifecycleObserver() { private boolean isCancelled(int errorCode) {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) return (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
public void onDestroy() { errorCode == BiometricPrompt.ERROR_CANCELED ||
Log.i("Authenticate destroyed"); errorCode == BiometricPrompt.ERROR_USER_CANCELED);
ApplicationEx.getMainHandler().removeCallbacks(cancelPrompt); }
try {
prompt.cancelAuthentication();
} catch (Throwable ex) {
Log.e(ex);
}
owner.getLifecycle().removeObserver(this);
}
});
} else { private boolean isHardwareFailure(int errorCode) {
Log.i("Authenticate PIN"); return (errorCode == BiometricPrompt.ERROR_HW_UNAVAILABLE ||
final View dview = LayoutInflater.from(activity).inflate(R.layout.dialog_pin_ask, null); errorCode == BiometricPrompt.ERROR_NO_BIOMETRICS || // No fingerprints enrolled.
final EditText etPin = dview.findViewById(R.id.etPin); errorCode == BiometricPrompt.ERROR_HW_NOT_PRESENT ||
errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL);
}
});
etPin.setEnabled(false); prompt.authenticate(info.build());
final AlertDialog dialog = new AlertDialog.Builder(activity) final Runnable cancelPrompt = new RunnableEx("auth:cancelprompt") {
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void delegate() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); try {
String pin = prefs.getString("pin", ""); Log.i("Authenticate cancel prompt");
String entered = etPin.getText().toString(); prompt.cancelAuthentication();
} catch (Throwable ex) {
Log.i("Authenticate PIN ok=" + pin.equals(entered)); Log.e(ex);
if (pin.equals(entered)) {
prefs.edit()
.remove("pin_failure_at")
.remove("pin_failure_count")
.apply();
setAuthenticated(activity);
ApplicationEx.getMainHandler().post(authenticated);
} else {
int count = prefs.getInt("pin_failure_count", 0) + 1;
prefs.edit()
.putLong("pin_failure_at", new Date().getTime())
.putInt("pin_failure_count", count)
.apply();
ApplicationEx.getMainHandler().post(cancelled);
} }
} }
}) };
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override ApplicationEx.getMainHandler().postDelayed(cancelPrompt, 60 * 1000L);
public void onClick(DialogInterface dialog, int which) {
Log.i("Authenticate PIN cancelled"); owner.getLifecycle().addObserver(new LifecycleObserver() {
ApplicationEx.getMainHandler().post(cancelled); @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
} public void onDestroy() {
}) Log.i("Authenticate destroyed");
.setOnDismissListener(new DialogInterface.OnDismissListener() { ApplicationEx.getMainHandler().removeCallbacks(cancelPrompt);
@Override try {
public void onDismiss(DialogInterface dialog) { prompt.cancelAuthentication();
Log.i("Authenticate PIN dismissed"); } catch (Throwable ex) {
if (shouldAuthenticate(activity, false)) // Some Android versions call dismiss on OK Log.e(ex);
ApplicationEx.getMainHandler().post(cancelled); }
owner.getLifecycle().removeObserver(this);
} }
}) });
.create();
etPin.setOnEditorActionListener(new TextView.OnEditorActionListener() { } else {
@Override Log.i("Authenticate PIN");
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { final View dview = LayoutInflater.from(activity).inflate(R.layout.dialog_pin_ask, null);
if (actionId == EditorInfo.IME_ACTION_DONE) { final EditText etPin = dview.findViewById(R.id.etPin);
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
return true; etPin.setEnabled(false);
} else
return false; final AlertDialog dialog = new AlertDialog.Builder(activity)
} .setView(dview)
}); .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
String pin = prefs.getString("pin", "");
String entered = etPin.getText().toString();
Log.i("Authenticate PIN ok=" + pin.equals(entered));
if (pin.equals(entered)) {
prefs.edit()
.remove("pin_failure_at")
.remove("pin_failure_count")
.apply();
setAuthenticated(activity);
ApplicationEx.getMainHandler().post(authenticated);
} else {
int count = prefs.getInt("pin_failure_count", 0) + 1;
prefs.edit()
.putLong("pin_failure_at", new Date().getTime())
.putInt("pin_failure_count", count)
.apply();
ApplicationEx.getMainHandler().post(cancelled);
}
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.i("Authenticate PIN cancelled");
ApplicationEx.getMainHandler().post(cancelled);
}
})
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
Log.i("Authenticate PIN dismissed");
if (shouldAuthenticate(activity, false)) // Some Android versions call dismiss on OK
ApplicationEx.getMainHandler().post(cancelled);
}
})
.create();
try { etPin.setOnEditorActionListener(new TextView.OnEditorActionListener() {
dialog.show(); @Override
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
long pin_failure_at = prefs.getLong("pin_failure_at", 0); dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
int pin_failure_count = prefs.getInt("pin_failure_count", 0); return true;
long wait = (long) Math.pow(PIN_FAILURE_DELAY, pin_failure_count) * 1000L; } else
long delay = pin_failure_at + wait - new Date().getTime(); return false;
Log.i("PIN wait=" + wait + " delay=" + delay);
dview.postDelayed(new Runnable() {
@Override
public void run() {
try {
etPin.setCompoundDrawables(null, null, null, null);
etPin.setEnabled(true);
etPin.requestFocus();
showKeyboard(etPin);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
} catch (Throwable ex) {
Log.e(ex);
} }
} });
}, delay < 0 ? 0 : delay);
} catch (Throwable ex) { try {
Log.e(ex); dialog.show();
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
long pin_failure_at = prefs.getLong("pin_failure_at", 0);
int pin_failure_count = prefs.getInt("pin_failure_count", 0);
long wait = (long) Math.pow(PIN_FAILURE_DELAY, pin_failure_count) * 1000L;
long delay = pin_failure_at + wait - new Date().getTime();
Log.i("PIN wait=" + wait + " delay=" + delay);
dview.postDelayed(new Runnable() {
@Override
public void run() {
try {
etPin.setCompoundDrawables(null, null, null, null);
etPin.setEnabled(true);
etPin.requestFocus();
showKeyboard(etPin);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
} catch (Throwable ex) {
Log.e(ex);
}
}
}, delay < 0 ? 0 : delay);
} catch (Throwable ex) {
Log.e(ex);
/* /*
java.lang.RuntimeException: Unable to start activity ComponentInfo{eu.faircode.email/eu.faircode.email.ActivityMain}: java.lang.RuntimeException: InputChannel is not initialized. java.lang.RuntimeException: Unable to start activity ComponentInfo{eu.faircode.email/eu.faircode.email.ActivityMain}: java.lang.RuntimeException: InputChannel is not initialized.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3477) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3477)
@ -2867,8 +2872,10 @@ public class Helper {
at eu.faircode.email.ActivityMain.onCreate(SourceFile:24) at eu.faircode.email.ActivityMain.onCreate(SourceFile:24)
at android.app.Activity.performCreate(Activity.java:7822) at android.app.Activity.performCreate(Activity.java:7822)
*/ */
}
}
} }
} });
} }
static void setAuthenticated(Context context) { static void setAuthenticated(Context context) {

Loading…
Cancel
Save