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,
Boolean enabled, final
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);
String pin = prefs.getString("pin", null);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
String pin = prefs.getString("pin", null);
if (enabled != null || TextUtils.isEmpty(pin)) {
Log.i("Authenticate biometric enabled=" + enabled);
BiometricPrompt.PromptInfo.Builder info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(enabled == null ? R.string.app_name : R.string.title_setup_biometrics));
if (enabled != null || TextUtils.isEmpty(pin)) {
Log.i("Authenticate biometric enabled=" + enabled);
BiometricPrompt.PromptInfo.Builder info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(enabled == null ? R.string.app_name : R.string.title_setup_biometrics));
KeyguardManager kgm = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kgm != null && kgm.isDeviceSecure())
info.setDeviceCredentialAllowed(true);
else
info.setNegativeButtonText(activity.getString(android.R.string.cancel));
info.setConfirmationRequired(false);
KeyguardManager kgm = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kgm != null && kgm.isDeviceSecure())
info.setDeviceCredentialAllowed(true);
else
info.setNegativeButtonText(activity.getString(android.R.string.cancel));
info.setSubtitle(activity.getString(enabled == null ? R.string.title_setup_biometrics_unlock
: enabled
? R.string.title_setup_biometrics_disable
: R.string.title_setup_biometrics_enable));
info.setConfirmationRequired(false);
final BiometricPrompt prompt = new BiometricPrompt(activity, executor,
new BiometricPrompt.AuthenticationCallback() {
private int fails = 0;
info.setSubtitle(activity.getString(enabled == null ? R.string.title_setup_biometrics_unlock
: enabled
? R.string.title_setup_biometrics_disable
: R.string.title_setup_biometrics_enable));
@Override
public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) {
if (isCancelled(errorCode) || errorCode == BiometricPrompt.ERROR_UNABLE_TO_PROCESS)
Log.w("Authenticate biometric error " + errorCode + ": " + errString);
else
Log.e("Authenticate biometric error " + errorCode + ": " + errString);
final BiometricPrompt prompt = new BiometricPrompt(activity, executor,
new BiometricPrompt.AuthenticationCallback() {
private int fails = 0;
if (isHardwareFailure(errorCode)) {
prefs.edit().remove("biometrics").apply();
ApplicationEx.getMainHandler().post(authenticated);
return;
}
@Override
public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) {
if (isCancelled(errorCode) || errorCode == BiometricPrompt.ERROR_UNABLE_TO_PROCESS)
Log.w("Authenticate biometric error " + errorCode + ": " + errString);
else
Log.e("Authenticate biometric error " + errorCode + ": " + errString);
if (!isCancelled(errorCode))
ApplicationEx.getMainHandler().post(new RunnableEx("auth:error") {
@Override
public void delegate() {
ToastEx.makeText(activity,
"Error " + errorCode + ": " + errString,
Toast.LENGTH_LONG).show();
if (isHardwareFailure(errorCode)) {
prefs.edit().remove("biometrics").apply();
ApplicationEx.getMainHandler().post(authenticated);
return;
}
});
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) {
return (errorCode == BiometricPrompt.ERROR_HW_UNAVAILABLE ||
errorCode == BiometricPrompt.ERROR_NO_BIOMETRICS || // No fingerprints enrolled.
errorCode == BiometricPrompt.ERROR_HW_NOT_PRESENT ||
errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL);
}
});
prompt.authenticate(info.build());
if (!isCancelled(errorCode))
ApplicationEx.getMainHandler().post(new RunnableEx("auth:error") {
@Override
public void delegate() {
ToastEx.makeText(activity,
"Error " + errorCode + ": " + errString,
Toast.LENGTH_LONG).show();
}
});
ApplicationEx.getMainHandler().post(cancelled);
}
final Runnable cancelPrompt = new RunnableEx("auth:cancelprompt") {
@Override
public void delegate() {
try {
prompt.cancelAuthentication();
} catch (Throwable ex) {
Log.e(ex);
}
}
};
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
Log.i("Authenticate biometric succeeded");
setAuthenticated(activity);
ApplicationEx.getMainHandler().post(authenticated);
}
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() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy() {
Log.i("Authenticate destroyed");
ApplicationEx.getMainHandler().removeCallbacks(cancelPrompt);
try {
prompt.cancelAuthentication();
} catch (Throwable ex) {
Log.e(ex);
}
owner.getLifecycle().removeObserver(this);
}
});
private boolean isCancelled(int errorCode) {
return (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
errorCode == BiometricPrompt.ERROR_CANCELED ||
errorCode == BiometricPrompt.ERROR_USER_CANCELED);
}
} else {
Log.i("Authenticate PIN");
final View dview = LayoutInflater.from(activity).inflate(R.layout.dialog_pin_ask, null);
final EditText etPin = dview.findViewById(R.id.etPin);
private boolean isHardwareFailure(int errorCode) {
return (errorCode == BiometricPrompt.ERROR_HW_UNAVAILABLE ||
errorCode == BiometricPrompt.ERROR_NO_BIOMETRICS || // No fingerprints enrolled.
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)
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
final Runnable cancelPrompt = new RunnableEx("auth:cancelprompt") {
@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);
public void delegate() {
try {
Log.i("Authenticate cancel prompt");
prompt.cancelAuthentication();
} catch (Throwable ex) {
Log.e(ex);
}
}
})
.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);
};
ApplicationEx.getMainHandler().postDelayed(cancelPrompt, 60 * 1000L);
owner.getLifecycle().addObserver(new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy() {
Log.i("Authenticate destroyed");
ApplicationEx.getMainHandler().removeCallbacks(cancelPrompt);
try {
prompt.cancelAuthentication();
} catch (Throwable ex) {
Log.e(ex);
}
owner.getLifecycle().removeObserver(this);
}
})
.create();
});
etPin.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
return true;
} else
return false;
}
});
} else {
Log.i("Authenticate PIN");
final View dview = LayoutInflater.from(activity).inflate(R.layout.dialog_pin_ask, null);
final EditText etPin = dview.findViewById(R.id.etPin);
etPin.setEnabled(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 {
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);
etPin.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
return true;
} else
return false;
}
}
}, delay < 0 ? 0 : delay);
});
} catch (Throwable ex) {
Log.e(ex);
try {
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.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3477)
@ -2867,8 +2872,10 @@ public class Helper {
at eu.faircode.email.ActivityMain.onCreate(SourceFile:24)
at android.app.Activity.performCreate(Activity.java:7822)
*/
}
}
}
}
});
}
static void setAuthenticated(Context context) {

Loading…
Cancel
Save