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:
cketti 2021-08-18 02:34:13 +02:00
parent 942d8e4a8f
commit cff6041dc8
7 changed files with 142 additions and 88 deletions

View file

@ -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}"
}

View file

@ -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
}
})
}
}

View file

@ -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"

View file

@ -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) {

View file

@ -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();

View file

@ -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>

View file

@ -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}"