Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cff6041d authored by cketti's avatar cketti
Browse files

Extract code for authenticated password toggle to separate file

Add support for orientation changes and mark screen as secure if password has been revealed.
parent 942d8e4a
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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}"
}
+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
                }
            })
        }
}
+0 −2
Original line number Diff line number Diff line
@@ -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"
+10 −42
Original line number Diff line number Diff line
@@ -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());
            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) {
+10 −42
Original line number Diff line number Diff line
@@ -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());
            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();
Loading