From b1dcc4f7a99185af020fcd1cc1ddd8637bafad5f Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 5 Jan 2016 12:18:22 +0100 Subject: [PATCH] add StopListening in OnPause of QuickUnlock add missing files --- src/keepass2android/FingerprintModule.cs | 434 ++++++++++++++++++ .../FingerprintSetupActivity.cs | 313 +++++++++++++ src/keepass2android/FingerprintUnlockMode.cs | 9 + src/keepass2android/QuickUnlock.cs | 9 + .../Resources/drawable-mdpi/ic_fp_40px.png | Bin 0 -> 4001 bytes .../Resources/drawable-xhdpi/ic_fp_40px.png | Bin 0 -> 10524 bytes .../drawable/ic_fingerprint_error.xml | 28 ++ .../drawable/ic_fingerprint_success.xml | 28 ++ .../Resources/layout/fingerprint_setup.xml | 115 +++++ 9 files changed, 936 insertions(+) create mode 100644 src/keepass2android/FingerprintModule.cs create mode 100644 src/keepass2android/FingerprintSetupActivity.cs create mode 100644 src/keepass2android/FingerprintUnlockMode.cs create mode 100644 src/keepass2android/Resources/drawable-mdpi/ic_fp_40px.png create mode 100644 src/keepass2android/Resources/drawable-xhdpi/ic_fp_40px.png create mode 100644 src/keepass2android/Resources/drawable/ic_fingerprint_error.xml create mode 100644 src/keepass2android/Resources/drawable/ic_fingerprint_success.xml create mode 100644 src/keepass2android/Resources/layout/fingerprint_setup.xml diff --git a/src/keepass2android/FingerprintModule.cs b/src/keepass2android/FingerprintModule.cs new file mode 100644 index 00000000..6abac0c9 --- /dev/null +++ b/src/keepass2android/FingerprintModule.cs @@ -0,0 +1,434 @@ +using System; +using Android.Content; +using Javax.Crypto; +using Java.Security; +using Java.Lang; +using Android.Views.InputMethods; +using Android.App; +using Android.Hardware.Fingerprints; +using Android.OS; +using Android.Security.Keystore; +using Android.Preferences; +using Android.Util; +using Android.Widget; +using Java.IO; +using Java.Security.Cert; +using Javax.Crypto.Spec; + +namespace keepass2android +{ + public class FingerprintModule + { + public Context Context { get; set; } + + public FingerprintModule (Context context) + { + Context = context; + } + + public FingerprintManager FingerprintManager + { + get { return (FingerprintManager) Context.GetSystemService(Context.FingerprintService); } + } + + public KeyguardManager KeyguardManager + { + get + { + return (KeyguardManager) Context.GetSystemService("keyguard"); + } + } + + + public KeyStore Keystore + { + get + { + try + { + return KeyStore.GetInstance("AndroidKeyStore"); + } + catch (KeyStoreException e) + { + throw new RuntimeException("Failed to get an instance of KeyStore", e); + } + } + } + + public KeyGenerator KeyGenerator + { + get + { + try + { + return KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, "AndroidKeyStore"); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException("Failed to get an instance of KeyGenerator", e); + } + catch (NoSuchProviderException e) + { + throw new RuntimeException("Failed to get an instance of KeyGenerator", e); + } + } + } + + public Cipher Cipher + { + get + { + try + { + return Cipher.GetInstance(KeyProperties.KeyAlgorithmAes + "/" + + KeyProperties.BlockModeCbc + "/" + + KeyProperties.EncryptionPaddingPkcs7); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException("Failed to get an instance of Cipher", e); + } + catch (NoSuchPaddingException e) + { + throw new RuntimeException("Failed to get an instance of Cipher", e); + } + } + } + + public InputMethodManager InputMethodManager + { + get { return (InputMethodManager) Context.GetSystemService(Context.InputMethodService); } + } + + public ISharedPreferences SharedPreferences + { + get { return PreferenceManager.GetDefaultSharedPreferences(Context); } + } + } + + public abstract class FingerprintCrypt: FingerprintManager.AuthenticationCallback + { + protected const string FailedToInitCipher = "Failed to init Cipher"; + public override void OnAuthenticationError(FingerprintState errorCode, ICharSequence errString) + { + Kp2aLog.Log("FP: OnAuthenticationError: " + errString + ", " + _selfCancelled); + if (!_selfCancelled) + _callback.OnAuthenticationError(errorCode, errString); + } + + public override void OnAuthenticationFailed() + { + Kp2aLog.Log("FP: OnAuthenticationFailed " + _selfCancelled); + _callback.OnAuthenticationFailed(); + } + + public override void OnAuthenticationHelp(FingerprintState helpCode, ICharSequence helpString) + { + _callback.OnAuthenticationHelp(helpCode, helpString); + } + + public override void OnAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) + { + Kp2aLog.Log("FP: OnAuthenticationSucceeded "); + StopListening(); + _callback.OnAuthenticationSucceeded(result); + } + + protected readonly string _keyId; + + protected Cipher _cipher; + private bool _selfCancelled; + private CancellationSignal _cancellationSignal; + protected FingerprintManager.CryptoObject _cryptoObject; + private FingerprintManager.AuthenticationCallback _callback; + protected KeyStore _keystore; + + private FingerprintManager _fingerprintManager; + + public FingerprintCrypt(FingerprintModule fingerprint, string keyId) + { + Kp2aLog.Log("FP: Create " + this.GetType().Name); + _keyId = keyId; + + _cipher = fingerprint.Cipher; + _keystore = fingerprint.Keystore; + + _fingerprintManager = fingerprint.FingerprintManager; + + } + + public abstract bool InitCipher(); + protected static string GetAlias(string keyId) + { + return "keepass2android." + keyId; + } + protected static string GetIvPrefKey(string prefKey) + { + return prefKey + "_iv"; + } + public bool IsFingerprintAuthAvailable + { + get + { + return _fingerprintManager.IsHardwareDetected + && _fingerprintManager.HasEnrolledFingerprints; + } + } + + public void StartListening(FingerprintManager.AuthenticationCallback callback) + { + if (!IsFingerprintAuthAvailable) + return; + + Kp2aLog.Log("FP: StartListening "); + _cancellationSignal = new CancellationSignal(); + _selfCancelled = false; + _callback = callback; + _fingerprintManager.Authenticate(_cryptoObject, _cancellationSignal, 0 /* flags */, this, null); + + } + + public void StopListening() + { + if (_cancellationSignal != null) + { + Kp2aLog.Log("FP: StopListening "); + _selfCancelled = true; + _cancellationSignal.Cancel(); + _cancellationSignal = null; + } + } + + public string Encrypt(string textToEncrypt) + { + Kp2aLog.Log("FP: Encrypting"); + return Base64.EncodeToString(_cipher.DoFinal(System.Text.Encoding.UTF8.GetBytes(textToEncrypt)), 0); + } + + + public void StoreEncrypted(string textToEncrypt, string prefKey, Context context) + { + var edit = PreferenceManager.GetDefaultSharedPreferences(context).Edit(); + StoreEncrypted(textToEncrypt, prefKey, edit); + edit.Commit(); + } + + public void StoreEncrypted(string textToEncrypt, string prefKey, ISharedPreferencesEditor edit) + { + edit.PutString(prefKey, Encrypt(textToEncrypt)); + edit.PutString(GetIvPrefKey(prefKey), Base64.EncodeToString(CipherIv, 0)); + + } + + + private byte[] CipherIv + { + get { return _cipher.GetIV(); } + } + } + + public class FingerprintDecryption : FingerprintCrypt + { + private readonly Context _context; + private readonly byte[] _iv; + + + public FingerprintDecryption(FingerprintModule fingerprint, string keyId, byte[] iv) : base(fingerprint, keyId) + { + _iv = iv; + } + + public FingerprintDecryption(FingerprintModule fingerprint, string keyId, Context context, string prefKey) + : base(fingerprint, keyId) + { + _context = context; + _iv = Base64.Decode(PreferenceManager.GetDefaultSharedPreferences(context).GetString(GetIvPrefKey(prefKey), null), 0); + } + + public override bool InitCipher() + { + Kp2aLog.Log("FP: InitCipher for Dec"); + try + { + _keystore.Load(null); + var key = _keystore.GetKey(GetAlias(_keyId), null); + var ivParams = new IvParameterSpec(_iv); + _cipher.Init(CipherMode.DecryptMode, key, ivParams); + + _cryptoObject = new FingerprintManager.CryptoObject(_cipher); + return true; + } + catch (KeyPermanentlyInvalidatedException) + { + return false; + } + catch (KeyStoreException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (CertificateException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (UnrecoverableKeyException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (IOException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (InvalidKeyException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + } + + + public string Decrypt(string encryted) + { + Kp2aLog.Log("FP: Decrypting "); + byte[] encryptedBytes = Base64.Decode(encryted, 0); + return System.Text.Encoding.UTF8.GetString(_cipher.DoFinal(encryptedBytes)); + } + + public string DecryptStored(string prefKey) + { + string enc = PreferenceManager.GetDefaultSharedPreferences(_context).GetString(prefKey, null); + return Decrypt(enc); + } + } + + public class FingerprintEncryption : FingerprintCrypt + { + + private KeyGenerator _keyGen; + + + public FingerprintEncryption(FingerprintModule fingerprint, string keyId) : + base(fingerprint, keyId) + { + _keyGen = fingerprint.KeyGenerator; + Kp2aLog.Log("FP: CreateKey "); + CreateKey(); + } + + + /// + /// Creates a symmetric key in the Android Key Store which can only be used after the user + /// has authenticated with fingerprint. + /// + private void CreateKey() + { + try + { + _keystore.Load(null); + _keyGen.Init(new KeyGenParameterSpec.Builder(GetAlias(_keyId), + KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt) + .SetBlockModes(KeyProperties.BlockModeCbc) + // Require the user to authenticate with a fingerprint to authorize every use + // of the key + .SetUserAuthenticationRequired(true) + .SetEncryptionPaddings(KeyProperties.EncryptionPaddingPkcs7) + .Build()); + _keyGen.GenerateKey(); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + catch (InvalidAlgorithmParameterException e) + { + throw new RuntimeException(e); + } + catch (CertificateException e) + { + throw new RuntimeException(e); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + public override bool InitCipher() + { + Kp2aLog.Log("FP: InitCipher for Enc "); + try + { + _keystore.Load(null); + var key = _keystore.GetKey(GetAlias(_keyId), null); + _cipher.Init(CipherMode.EncryptMode, key); + + _cryptoObject = new FingerprintManager.CryptoObject(_cipher); + return true; + } + catch (KeyPermanentlyInvalidatedException) + { + return false; + } + catch (KeyStoreException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (CertificateException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (UnrecoverableKeyException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (IOException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + catch (InvalidKeyException e) + { + throw new RuntimeException(FailedToInitCipher, e); + } + } + } + + public class FingerprintAuthCallbackAdapter : FingerprintManager.AuthenticationCallback + { + private readonly IFingerprintAuthCallback _callback; + private readonly Context _context; + + public FingerprintAuthCallbackAdapter(IFingerprintAuthCallback callback, Context context) + { + _callback = callback; + _context = context; + } + + public override void OnAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) + { + _callback.OnFingerprintAuthSucceeded(); + } + + public override void OnAuthenticationError(FingerprintState errorCode, ICharSequence errString) + { + _callback.OnFingerprintError(errString.ToString()); + } + + public override void OnAuthenticationHelp(FingerprintState helpCode, ICharSequence helpString) + { + _callback.OnFingerprintError(helpString.ToString()); + } + + public override void OnAuthenticationFailed() + { + _callback.OnFingerprintError( + _context.Resources.GetString(Resource.String.fingerprint_not_recognized)); + } + } + +} \ No newline at end of file diff --git a/src/keepass2android/FingerprintSetupActivity.cs b/src/keepass2android/FingerprintSetupActivity.cs new file mode 100644 index 00000000..aaef9ed7 --- /dev/null +++ b/src/keepass2android/FingerprintSetupActivity.cs @@ -0,0 +1,313 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Android; +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.Hardware.Fingerprints; +using Android.OS; +using Android.Preferences; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using Java.Lang; +using KeePassLib.Keys; +using KeePassLib.Utility; +using Enum = System.Enum; +using Exception = System.Exception; + +namespace keepass2android +{ + [Activity(Label = "@string/app_name", + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden, + Theme = "@style/MyTheme_ActionBar", MainLauncher = false)] + [IntentFilter(new[] { "kp2a.action.FingerprintSetupActivity" }, Categories = new[] { Intent.CategoryDefault })] + public class FingerprintSetupActivity : LockCloseActivity + { + private readonly ActivityDesign _activityDesign; + + public FingerprintSetupActivity(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) + { + _activityDesign = new ActivityDesign(this); + } + public FingerprintSetupActivity() + { + _activityDesign = new ActivityDesign(this); + } + + + + private FingerprintUnlockMode _unlockMode = FingerprintUnlockMode.Disabled; + private FingerprintUnlockMode _desiredUnlockMode; + private FingerprintEncryption _enc; + private RadioButton[] _radioButtons; + public override bool OnOptionsItemSelected(IMenuItem item) + { + switch (item.ItemId) + { + + case Android.Resource.Id.Home: + Finish(); + return true; + } + + return base.OnOptionsItemSelected(item); + } + + protected override void OnCreate(Bundle savedInstanceState) + { + _activityDesign.ApplyTheme(); + base.OnCreate(savedInstanceState); + SetContentView(Resource.Layout.fingerprint_setup); + + Enum.TryParse( + PreferenceManager.GetDefaultSharedPreferences(this).GetString(App.Kp2a.GetDb().CurrentFingerprintModePrefKey, ""), + out _unlockMode); + + _fpIcon = FindViewById(Resource.Id.fingerprint_icon); + _fpTextView = FindViewById(Resource.Id.fingerprint_status); + + SupportActionBar.SetDisplayHomeAsUpEnabled(true); + SupportActionBar.SetHomeButtonEnabled(true); + + int[] radioButtonIds = + { + Resource.Id.radio_fingerprint_quickunlock, Resource.Id.radio_fingerprint_unlock, + Resource.Id.radio_fingerprint_disabled + }; + _radioButtons = radioButtonIds.Select(FindViewById).ToArray(); + _radioButtons[0].Tag = FingerprintUnlockMode.QuickUnlock.ToString(); + _radioButtons[1].Tag = FingerprintUnlockMode.FullUnlock.ToString(); + _radioButtons[2].Tag = FingerprintUnlockMode.Disabled.ToString(); + foreach (RadioButton r in _radioButtons) + { + r.CheckedChange += (sender, args) => + { + var rbSender = ((RadioButton) sender); + if (!rbSender.Checked) return; + foreach (RadioButton rOther in _radioButtons) + { + if (rOther == sender) continue; + rOther.Checked = false; + } + FingerprintUnlockMode newMode; + Enum.TryParse(rbSender.Tag.ToString(), out newMode); + ChangeUnlockMode(_unlockMode, newMode); + + }; + } + + CheckCurrentRadioButton(); + + int errorId = Resource.String.fingerprint_os_error; + SetError(errorId); + + FindViewById(Resource.Id.cancel_button).Click += (sender, args) => + { + _enc.StopListening(); + _unlockMode = FingerprintUnlockMode.Disabled; //cancelling a FingerprintEncryption means a new key has been created but not been authenticated to encrypt something. We can't keep the previous state. + StoreUnlockMode(); + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + _enc = null; + CheckCurrentRadioButton(); + }; + + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + + if ((int)Build.VERSION.SdkInt >= 23) + RequestPermissions(new[] { Manifest.Permission.UseFingerprint }, FingerprintPermissionRequestCode); + } + + string CurrentPreferenceKey + { + get { return App.Kp2a.GetDb().CurrentFingerprintPrefKey; } + } + + private void StoreUnlockMode() + { + ISharedPreferencesEditor edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit(); + if (_unlockMode == FingerprintUnlockMode.Disabled) + { + edit.PutString(CurrentPreferenceKey, ""); + } + else + { + if (_unlockMode == FingerprintUnlockMode.FullUnlock) + _enc.StoreEncrypted(App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey().Password.ReadString(), CurrentPreferenceKey, edit); + else + _enc.StoreEncrypted("QuickUnlock" /*some dummy data*/, CurrentPreferenceKey, edit); + } + edit.PutString(App.Kp2a.GetDb().CurrentFingerprintModePrefKey, _unlockMode.ToString()); + edit.Commit(); + } + + private void CheckCurrentRadioButton() + { + + foreach (RadioButton r in _radioButtons) + { + FingerprintUnlockMode um; + Enum.TryParse(r.Tag.ToString(), out um); + if (um == _unlockMode) + r.Checked = true; + } + } + + private void SetError(int errorId) + { + var tv = FindViewById(Resource.Id.tvFatalError); + tv.Text = GetString(Resource.String.fingerprint_fatal) + " " + GetString(errorId); + tv.Visibility = ViewStates.Visible; + } + + const int FingerprintPermissionRequestCode = 0; + public override void OnRequestPermissionsResult (int requestCode, string[] permissions, Permission[] grantResults) + { + if (requestCode == FingerprintPermissionRequestCode && grantResults[0] == Permission.Granted) + { + FingerprintModule fpModule = new FingerprintModule(this); + if (!fpModule.FingerprintManager.IsHardwareDetected) + { + SetError(Resource.String.fingerprint_hardware_error); + return; + } + if (!fpModule.FingerprintManager.HasEnrolledFingerprints) + { + SetError(Resource.String.fingerprint_no_enrolled); + return; + } + FindViewById(Resource.Id.tvFatalError).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + } + } + + + private void ChangeUnlockMode(FingerprintUnlockMode oldMode, FingerprintUnlockMode newMode) + { + if (oldMode == newMode) + return; + + if (newMode == FingerprintUnlockMode.Disabled) + { + _unlockMode = newMode; + StoreUnlockMode(); + return; + } + + _desiredUnlockMode = newMode; + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Visible; + + _enc = new FingerprintEncryption(new FingerprintModule(this), CurrentPreferenceKey); + try + { + if (!_enc.InitCipher()) + throw new Exception("Failed to initialize cipher"); + ResetErrorTextRunnable(); + _enc.StartListening(new SetupCallback(this)); + } + catch (Exception e) + { + CheckCurrentRadioButton(); + Toast.MakeText(this, e.ToString(), ToastLength.Long).Show(); + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + } + + + } + + static readonly long ERROR_TIMEOUT_MILLIS = 1600; + static readonly long SUCCESS_DELAY_MILLIS = 1300; + private ImageView _fpIcon; + private TextView _fpTextView; + public void OnAuthSucceeded() + { + _unlockMode = _desiredUnlockMode; + + _fpTextView.RemoveCallbacks(ResetErrorTextRunnable); + _fpIcon.SetImageResource(Resource.Drawable.ic_fingerprint_success); + _fpTextView.SetTextColor(_fpTextView.Resources.GetColor(Resource.Color.success_color, null)); + _fpTextView.Text = _fpTextView.Resources.GetString(Resource.String.fingerprint_success); + _fpIcon.PostDelayed(() => + { + FindViewById(Resource.Id.radio_buttons).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.fingerprint_auth_container).Visibility = ViewStates.Gone; + + StoreUnlockMode(); + + }, SUCCESS_DELAY_MILLIS); + + + } + + + public void OnFingerprintError(string error) + { + _fpIcon.SetImageResource(Resource.Drawable.ic_fingerprint_error); + _fpTextView.Text = error; + _fpTextView.SetTextColor( + _fpTextView.Resources.GetColor(Resource.Color.warning_color, null)); + _fpTextView.RemoveCallbacks(ResetErrorTextRunnable); + _fpTextView.PostDelayed(ResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS); + } + + void ResetErrorTextRunnable() + { + _fpTextView.SetTextColor( + _fpTextView.Resources.GetColor(Resource.Color.hint_color, null)); + _fpTextView.Text = _fpTextView.Resources.GetString(Resource.String.fingerprint_hint); + _fpIcon.SetImageResource(Resource.Drawable.ic_fp_40px); + } + + protected override void OnResume() + { + base.OnResume(); + if (_enc != null) + _enc.StartListening(new SetupCallback(this)); + } + + protected override void OnPause() + { + base.OnPause(); + if (_enc != null) + _enc.StopListening(); + } + } + + internal class SetupCallback : FingerprintManager.AuthenticationCallback + { + private readonly FingerprintSetupActivity _fingerprintSetupActivity; + + public SetupCallback(FingerprintSetupActivity fingerprintSetupActivity) + { + _fingerprintSetupActivity = fingerprintSetupActivity; + } + + public override void OnAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) + { + _fingerprintSetupActivity.OnAuthSucceeded(); + } + + public override void OnAuthenticationError(FingerprintState errorCode, ICharSequence errString) + { + _fingerprintSetupActivity.OnFingerprintError(errString.ToString()); + } + + public override void OnAuthenticationHelp(FingerprintState helpCode, ICharSequence helpString) + { + _fingerprintSetupActivity.OnFingerprintError(helpString.ToString()); + } + + public override void OnAuthenticationFailed() + { + _fingerprintSetupActivity.OnFingerprintError(_fingerprintSetupActivity.Resources.GetString(Resource.String.fingerprint_not_recognized)); + } + } +} \ No newline at end of file diff --git a/src/keepass2android/FingerprintUnlockMode.cs b/src/keepass2android/FingerprintUnlockMode.cs new file mode 100644 index 00000000..bd80f262 --- /dev/null +++ b/src/keepass2android/FingerprintUnlockMode.cs @@ -0,0 +1,9 @@ +namespace keepass2android +{ + public enum FingerprintUnlockMode + { + Disabled = 0, + QuickUnlock = 1, + FullUnlock = 2, + } +} \ No newline at end of file diff --git a/src/keepass2android/QuickUnlock.cs b/src/keepass2android/QuickUnlock.cs index ce11d5b6..088c46ba 100644 --- a/src/keepass2android/QuickUnlock.cs +++ b/src/keepass2android/QuickUnlock.cs @@ -305,6 +305,15 @@ namespace keepass2android } } + protected override void OnPause() + { + base.OnPause(); + if (_fingerprintDec != null) + { + _fingerprintDec.StopListening(); + } + } + protected override void OnDestroy() { base.OnDestroy(); diff --git a/src/keepass2android/Resources/drawable-mdpi/ic_fp_40px.png b/src/keepass2android/Resources/drawable-mdpi/ic_fp_40px.png new file mode 100644 index 0000000000000000000000000000000000000000..122f44257b1aa188955ab01e5bf71a5f079b1da5 GIT binary patch literal 4001 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 literal 0 HcmV?d00001 diff --git a/src/keepass2android/Resources/drawable-xhdpi/ic_fp_40px.png b/src/keepass2android/Resources/drawable-xhdpi/ic_fp_40px.png new file mode 100644 index 0000000000000000000000000000000000000000..e1c9590bbfbcf220b08879f2f9e560a4f285f4ec GIT binary patch literal 10524 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 + + + + + diff --git a/src/keepass2android/Resources/drawable/ic_fingerprint_success.xml b/src/keepass2android/Resources/drawable/ic_fingerprint_success.xml new file mode 100644 index 00000000..261f3e7f --- /dev/null +++ b/src/keepass2android/Resources/drawable/ic_fingerprint_success.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/src/keepass2android/Resources/layout/fingerprint_setup.xml b/src/keepass2android/Resources/layout/fingerprint_setup.xml new file mode 100644 index 00000000..40b8441c --- /dev/null +++ b/src/keepass2android/Resources/layout/fingerprint_setup.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + +