Merge pull request #5584 from ByteHamster/authenticate-before-password
Authenticate user before showing password
This commit is contained in:
commit
3b42061377
10 changed files with 149 additions and 6 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -29,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"
|
||||
|
|
|
@ -19,11 +19,9 @@ import android.widget.CheckBox;
|
|||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.Account.FolderMode;
|
||||
import com.fsck.k9.DI;
|
||||
import com.fsck.k9.LocalKeyStoreManager;
|
||||
import com.fsck.k9.Preferences;
|
||||
|
@ -44,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;
|
||||
|
||||
|
@ -181,6 +180,15 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener
|
|||
}
|
||||
|
||||
boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction());
|
||||
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();
|
||||
|
|
|
@ -34,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;
|
||||
|
@ -148,6 +149,17 @@ public class AccountSetupOutgoing extends K9Activity implements OnClickListener,
|
|||
mAccount = Preferences.getPreferences(this).getAccount(accountUuid);
|
||||
}
|
||||
|
||||
boolean editSettings = Intent.ACTION_EDIT.equals(getIntent().getAction());
|
||||
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();
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/account_setup_margin_between_items_incoming_and_outgoing"
|
||||
app:passwordToggleEnabled="true">
|
||||
app:endIconMode="password_toggle">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/account_password"
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/account_setup_margin_between_items_incoming_and_outgoing"
|
||||
app:passwordToggleEnabled="true">
|
||||
app:endIconMode="password_toggle">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/account_password"
|
||||
|
|
|
@ -355,6 +355,9 @@ 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 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>
|
||||
|
||||
<string name="account_setup_check_settings_title"/>
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -17,6 +17,7 @@ buildscript {
|
|||
'androidxRecyclerView': '1.1.0',
|
||||
'androidxLifecycle': '2.3.1',
|
||||
'androidxAnnotation': '1.2.0',
|
||||
'androidxBiometric': '1.1.0',
|
||||
'androidxNavigation': '2.3.5',
|
||||
'androidxConstraintLayout': '2.0.4',
|
||||
'androidxWorkManager': '2.5.0',
|
||||
|
|
Loading…
Reference in a new issue