Loading app/ui/base/build.gradle +2 −0 Original line number Diff line number Diff line Loading @@ -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}" } Loading app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt 0 → 100644 +119 −0 Original line number Diff line number Diff line @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 } }) } } app/ui/legacy/build.gradle +0 −1 Original line number Diff line number Diff line Loading @@ -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" Loading app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +10 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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(); Loading app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java +12 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); Loading Loading
app/ui/base/build.gradle +2 −0 Original line number Diff line number Diff line Loading @@ -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}" } Loading
app/ui/base/src/main/java/com/fsck/k9/ui/base/extensions/TextInputLayoutExtensions.kt 0 → 100644 +119 −0 Original line number Diff line number Diff line @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 } }) } }
app/ui/legacy/build.gradle +0 −1 Original line number Diff line number Diff line Loading @@ -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" Loading
app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupIncoming.java +10 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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(); Loading
app/ui/legacy/src/main/java/com/fsck/k9/activity/setup/AccountSetupOutgoing.java +12 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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(); Loading