Extract code for authenticated password toggle to separate file
Add support for orientation changes and mark screen as secure if password has been revealed.
This commit is contained in:
parent
942d8e4a8f
commit
cff6041dc8
7 changed files with 142 additions and 88 deletions
|
@ -5,11 +5,13 @@ dependencies {
|
|||
implementation project(":app:core")
|
||||
|
||||
api "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
|
||||
api "com.google.android.material:material:${versions.materialComponents}"
|
||||
api "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}"
|
||||
api "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}"
|
||||
api "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
|
||||
|
||||
implementation "androidx.core:core-ktx:${versions.androidxCore}"
|
||||
implementation "androidx.biometric:biometric:${versions.androidxBiometric}"
|
||||
implementation "com.jakewharton.timber:timber:${versions.timber}"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
@file:JvmName("TextInputLayoutHelper")
|
||||
|
||||
package com.fsck.k9.ui.base.extensions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
/**
|
||||
* Configures a [TextInputLayout] so the password can only be revealed after authentication.
|
||||
*/
|
||||
fun TextInputLayout.configureAuthenticatedPasswordToggle(
|
||||
activity: FragmentActivity,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
needScreenLockMessage: String,
|
||||
) {
|
||||
val viewModel = ViewModelProvider(activity).get(AuthenticatedPasswordToggleViewModel::class.java)
|
||||
viewModel.textInputLayout = this
|
||||
viewModel.activity = activity
|
||||
|
||||
fun authenticateUserAndShowPassword(activity: FragmentActivity) {
|
||||
val mainExecutor = ContextCompat.getMainExecutor(activity)
|
||||
|
||||
val context = activity.applicationContext
|
||||
val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
// The Activity might have been recreated since this callback object was created (e.g. due to an
|
||||
// orientation change). So we fetch the (new) references from the ViewModel.
|
||||
viewModel.isAuthenticated = true
|
||||
viewModel.activity?.setSecure(true)
|
||||
viewModel.textInputLayout?.editText?.showPassword()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
if (errorCode == BiometricPrompt.ERROR_HW_NOT_PRESENT ||
|
||||
errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL ||
|
||||
errorCode == BiometricPrompt.ERROR_NO_BIOMETRICS
|
||||
) {
|
||||
Toast.makeText(context, needScreenLockMessage, Toast.LENGTH_SHORT).show()
|
||||
} else if (errString.isNotEmpty()) {
|
||||
Toast.makeText(context, errString, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BiometricPrompt(activity, mainExecutor, authenticationCallback).authenticate(
|
||||
BiometricPrompt.PromptInfo.Builder()
|
||||
.setAllowedAuthenticators(BIOMETRIC_STRONG or BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
val editText = this.editText ?: error("TextInputLayout.editText == null")
|
||||
|
||||
setEndIconOnClickListener {
|
||||
if (editText.isPasswordHidden) {
|
||||
if (viewModel.isAuthenticated) {
|
||||
activity.setSecure(true)
|
||||
editText.showPassword()
|
||||
} else {
|
||||
authenticateUserAndShowPassword(activity)
|
||||
}
|
||||
} else {
|
||||
viewModel.isAuthenticated = false
|
||||
editText.hidePassword()
|
||||
activity.setSecure(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val EditText.isPasswordHidden: Boolean
|
||||
get() = transformationMethod is PasswordTransformationMethod
|
||||
|
||||
private fun EditText.showPassword() {
|
||||
transformationMethod = null
|
||||
}
|
||||
|
||||
private fun EditText.hidePassword() {
|
||||
transformationMethod = PasswordTransformationMethod.getInstance()
|
||||
}
|
||||
|
||||
private fun FragmentActivity.setSecure(secure: Boolean) {
|
||||
window.setFlags(if (secure) FLAG_SECURE else 0, FLAG_SECURE)
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
class AuthenticatedPasswordToggleViewModel : ViewModel() {
|
||||
var isAuthenticated = false
|
||||
var textInputLayout: TextInputLayout? = null
|
||||
var activity: FragmentActivity? = null
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
value?.lifecycle?.addObserver(object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
fun removeReferences() {
|
||||
textInputLayout = null
|
||||
field = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -22,7 +22,6 @@ dependencies {
|
|||
implementation "com.takisoft.preferencex:preferencex-datetimepicker:${versions.preferencesFix}"
|
||||
implementation "com.takisoft.preferencex:preferencex-colorpicker:${versions.preferencesFix}"
|
||||
implementation "com.takisoft.preferencex:preferencex-ringtone:${versions.preferencesFix}"
|
||||
implementation "androidx.biometric:biometric:${versions.androidxBiometric}"
|
||||
implementation "androidx.recyclerview:recyclerview:${versions.androidxRecyclerView}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
|
||||
|
@ -30,7 +29,6 @@ dependencies {
|
|||
implementation "androidx.cardview:cardview:${versions.androidxCardView}"
|
||||
implementation "androidx.localbroadcastmanager:localbroadcastmanager:${versions.androidxLocalBroadcastManager}"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "com.google.android.material:material:${versions.materialComponents}"
|
||||
implementation "de.cketti.library.changelog:ckchangelog-core:2.0.0-beta02"
|
||||
implementation "com.splitwise:tokenautocomplete:4.0.0-beta01"
|
||||
implementation "de.cketti.safecontentresolver:safe-content-resolver-v21:1.0.0"
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.os.Bundle;
|
|||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.DigitsKeyListener;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -22,10 +21,6 @@ import android.widget.CompoundButton.OnCheckedChangeListener;
|
|||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricManager.Authenticators;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.DI;
|
||||
import com.fsck.k9.LocalKeyStoreManager;
|
||||
|
@ -47,6 +42,7 @@ import com.fsck.k9.mail.store.imap.ImapStoreSettings;
|
|||
import com.fsck.k9.mail.store.webdav.WebDavStoreSettings;
|
||||
import com.fsck.k9.preferences.Protocols;
|
||||
import com.fsck.k9.ui.R;
|
||||
import com.fsck.k9.ui.base.extensions.TextInputLayoutHelper;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
|
||||
|
||||
|
@ -184,18 +180,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||
}
|
||||
|
||||
boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction());
|
||||
|
||||
mPasswordLayoutView.setEndIconOnClickListener(v -> {
|
||||
if (mPasswordView.getTransformationMethod() instanceof PasswordTransformationMethod) {
|
||||
if (editSettings) {
|
||||
authenticateUserAndShowPassword();
|
||||
} else {
|
||||
mPasswordView.setTransformationMethod(null);
|
||||
}
|
||||
} else {
|
||||
mPasswordView.setTransformationMethod(PasswordTransformationMethod.getInstance());
|
||||
}
|
||||
});
|
||||
if (editSettings) {
|
||||
TextInputLayoutHelper.configureAuthenticatedPasswordToggle(
|
||||
mPasswordLayoutView,
|
||||
this,
|
||||
getString(R.string.account_setup_basics_show_password_biometrics_title),
|
||||
getString(R.string.account_setup_basics_show_password_biometrics_subtitle),
|
||||
getString(R.string.account_setup_basics_show_password_need_lock)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
ServerSettings settings = mAccount.getIncomingServerSettings();
|
||||
|
@ -631,31 +624,6 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||
|
||||
}
|
||||
|
||||
private void authenticateUserAndShowPassword() {
|
||||
new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
mPasswordView.setTransformationMethod(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
if (errorCode == BiometricPrompt.ERROR_HW_NOT_PRESENT
|
||||
|| errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL) {
|
||||
Toast.makeText(AccountSetupIncoming.this, R.string.account_setup_basics_show_password_need_lock,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
} else if (errString.length() != 0) {
|
||||
Toast.makeText(AccountSetupIncoming.this, errString, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}).authenticate(new BiometricPrompt.PromptInfo.Builder()
|
||||
.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG
|
||||
| Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL)
|
||||
.setTitle(getString(R.string.account_setup_basics_show_password_biometrics_title))
|
||||
.setSubtitle(getString(R.string.account_setup_basics_show_password_biometrics_subtitle))
|
||||
.build());
|
||||
}
|
||||
|
||||
public void onClick(View v) {
|
||||
try {
|
||||
if (v.getId() == R.id.next) {
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.os.Bundle;
|
|||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.DigitsKeyListener;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -21,10 +20,6 @@ import android.widget.CompoundButton.OnCheckedChangeListener;
|
|||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.biometric.BiometricManager.Authenticators;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.DI;
|
||||
import com.fsck.k9.LocalKeyStoreManager;
|
||||
|
@ -39,6 +34,7 @@ import com.fsck.k9.mail.AuthType;
|
|||
import com.fsck.k9.mail.ConnectionSecurity;
|
||||
import com.fsck.k9.mail.MailServerDirection;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.ui.base.extensions.TextInputLayoutHelper;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner;
|
||||
import com.fsck.k9.view.ClientCertificateSpinner.OnClientCertificateChangedListener;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
@ -154,18 +150,15 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||
}
|
||||
|
||||
boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction());
|
||||
|
||||
mPasswordLayoutView.setEndIconOnClickListener(v -> {
|
||||
if (mPasswordView.getTransformationMethod() instanceof PasswordTransformationMethod) {
|
||||
if (editSettings) {
|
||||
authenticateUserAndShowPassword();
|
||||
} else {
|
||||
mPasswordView.setTransformationMethod(null);
|
||||
}
|
||||
} else {
|
||||
mPasswordView.setTransformationMethod(PasswordTransformationMethod.getInstance());
|
||||
}
|
||||
});
|
||||
if (editSettings) {
|
||||
TextInputLayoutHelper.configureAuthenticatedPasswordToggle(
|
||||
mPasswordLayoutView,
|
||||
this,
|
||||
getString(R.string.account_setup_basics_show_password_biometrics_title),
|
||||
getString(R.string.account_setup_basics_show_password_biometrics_subtitle),
|
||||
getString(R.string.account_setup_basics_show_password_need_lock)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
ServerSettings settings = mAccount.getOutgoingServerSettings();
|
||||
|
@ -517,31 +510,6 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, CheckDirection.OUTGOING);
|
||||
}
|
||||
|
||||
private void authenticateUserAndShowPassword() {
|
||||
new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
mPasswordView.setTransformationMethod(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
if (errorCode == BiometricPrompt.ERROR_HW_NOT_PRESENT
|
||||
|| errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL) {
|
||||
Toast.makeText(AccountSetupOutgoing.this, R.string.account_setup_basics_show_password_need_lock,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
} else if (errString.length() != 0) {
|
||||
Toast.makeText(AccountSetupOutgoing.this, errString, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}).authenticate(new BiometricPrompt.PromptInfo.Builder()
|
||||
.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG
|
||||
| Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL)
|
||||
.setTitle(getString(R.string.account_setup_basics_show_password_biometrics_title))
|
||||
.setSubtitle(getString(R.string.account_setup_basics_show_password_biometrics_subtitle))
|
||||
.build());
|
||||
}
|
||||
|
||||
public void onClick(View v) {
|
||||
if (v.getId() == R.id.next) {
|
||||
onNext();
|
||||
|
|
|
@ -355,7 +355,7 @@ Please submit bug reports, contribute new features and ask questions at
|
|||
<string name="account_setup_basics_email_hint">Email address</string>
|
||||
<string name="account_setup_basics_password_hint">Password</string>
|
||||
<string name="account_setup_basics_show_password">Show password</string>
|
||||
<string name="account_setup_basics_show_password_need_lock">To view or copy your password here, enable screen lock on this device.</string>
|
||||
<string name="account_setup_basics_show_password_need_lock">To view your password here, enable screen lock on this device.</string>
|
||||
<string name="account_setup_basics_show_password_biometrics_title">Verify it\'s you</string>
|
||||
<string name="account_setup_basics_show_password_biometrics_subtitle">Unlock to view your password</string>
|
||||
<string name="account_setup_basics_manual_setup_action">Manual setup</string>
|
||||
|
|
|
@ -10,7 +10,6 @@ dependencies {
|
|||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidxLifecycle}"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
|
||||
implementation "androidx.constraintlayout:constraintlayout:${versions.androidxConstraintLayout}"
|
||||
implementation "com.google.android.material:material:${versions.materialComponents}"
|
||||
implementation "androidx.core:core-ktx:${versions.androidxCore}"
|
||||
implementation "com.jakewharton.timber:timber:${versions.timber}"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}"
|
||||
|
|
Loading…
Reference in a new issue