Require Authentication to turn on lock with biometrics

Slightly different from upstream, only prompt if turning on a setting, since turning off means you've already bypassed the biometrics anyway, along with the fixes made after the initial commits

Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2021-10-17 05:09:54 -04:00
parent e93e538600
commit 31fbb11c4a
6 changed files with 134 additions and 17 deletions

View file

@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -102,7 +103,7 @@ open class App : Application(), DefaultLifecycleObserver {
}
override fun onPause(owner: LifecycleOwner) {
if (!SecureActivityDelegate.isAuthenticating && preferences.lockAfter().get() >= 0) {
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAfter().get() >= 0) {
SecureActivityDelegate.locked = true
}
}

View file

@ -5,6 +5,7 @@ import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import java.util.Date
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@ -15,7 +16,7 @@ class BiometricActivity : BaseThemedActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val fromSearch = intent.getBooleanExtra("fromSearch", false)
SecureActivityDelegate.isAuthenticating = true
AuthenticatorUtil.isAuthenticating = true
val biometricPrompt = BiometricPrompt(
this,
executor,
@ -24,7 +25,7 @@ class BiometricActivity : BaseThemedActivity() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
SecureActivityDelegate.isAuthenticating = false
AuthenticatorUtil.isAuthenticating = false
if (fromSearch) finish()
else finishAffinity()
}
@ -32,7 +33,7 @@ class BiometricActivity : BaseThemedActivity() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
SecureActivityDelegate.locked = false
SecureActivityDelegate.isAuthenticating = false
AuthenticatorUtil.isAuthenticating = false
preferences.lastUnlock().set(Date().time)
finish()
}

View file

@ -6,6 +6,7 @@ import android.view.WindowManager
import androidx.biometric.BiometricManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.SearchActivity
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import uy.kohesive.injekt.injectLazy
import java.util.Date
@ -14,7 +15,6 @@ object SecureActivityDelegate {
private val preferences by injectLazy<PreferencesHelper>()
var locked: Boolean = true
var isAuthenticating: Boolean = false
fun setSecure(activity: Activity?, force: Boolean? = null) {
val enabled = force ?: preferences.secureScreen().get()
@ -29,7 +29,7 @@ object SecureActivityDelegate {
}
fun promptLockIfNeeded(activity: Activity?, requireSuccess: Boolean = false) {
if (activity == null || isAuthenticating) return
if (activity == null || AuthenticatorUtil.isAuthenticating) return
val lockApp = preferences.useBiometrics().get()
if (lockApp && BiometricManager.from(activity).canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) {
if (isAppLocked()) {

View file

@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.ui.setting
import android.app.Activity
import androidx.annotation.StringRes
import androidx.biometric.BiometricPrompt
import androidx.core.graphics.drawable.DrawableCompat
import androidx.fragment.app.FragmentActivity
import androidx.preference.CheckBoxPreference
import androidx.preference.DialogPreference
import androidx.preference.DropDownPreference
@ -15,7 +17,11 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.preference.IntListMatPreference
import eu.kanade.tachiyomi.widget.preference.ListMatPreference
import eu.kanade.tachiyomi.widget.preference.MultiListMatPreference
@ -157,6 +163,36 @@ inline fun Preference.onChange(crossinline block: (Any?) -> Boolean) {
setOnPreferenceChangeListener { _, newValue -> block(newValue) }
}
fun SwitchPreferenceCompat.requireAuthentication(
activity: FragmentActivity?,
title: String,
subtitle: String? = null
) {
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
newValue as Boolean
if (newValue && activity != null && context.isAuthenticationSupported()) {
activity.startAuthentication(
title,
subtitle,
callback = object : AuthenticatorUtil.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
isChecked = newValue
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
activity.toast(errString.toString())
}
}
)
false
} else {
true
}
}
}
var Preference.defaultValue: Any?
get() = null // set only
set(value) { setDefaultValue(value) }

View file

@ -1,33 +1,31 @@
package eu.kanade.tachiyomi.ui.setting
import androidx.biometric.BiometricManager
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.widget.preference.IntListMatPreference
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
class SettingsSecurityController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.security
val biometricManager = BiometricManager.from(context)
if (biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
var preference: IntListMatPreference? = null
if (context.isAuthenticationSupported()) {
switchPreference {
key = PreferenceKeys.useBiometrics
titleRes = R.string.lock_with_biometrics
defaultValue = false
onChange {
preference?.isVisible = it as Boolean
true
}
requireAuthentication(
activity as? FragmentActivity,
activity!!.getString(R.string.lock_with_biometrics)
)
}
preference = intListPreference(activity) {
intListPreference(activity) {
key = PreferenceKeys.lockAfter
titleRes = R.string.lock_when_idle
isVisible = preferences.useBiometrics().get()
val values = listOf(0, 2, 5, 10, 20, 30, 60, 90, 120, -1)
entries = values.mapNotNull {
when (it) {
@ -42,6 +40,8 @@ class SettingsSecurityController : SettingsController() {
}
entryValues = values
defaultValue = 0
preferences.useBiometrics().asImmediateFlowIn(viewScope) { isVisible = it }
}
}

View file

@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.util.system
import android.content.Context
import androidx.annotation.CallSuper
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import java.util.concurrent.Executor
object AuthenticatorUtil {
/**
* A check to avoid double authentication on older APIs when confirming settings changes since
* the biometric prompt is launched in a separate activity outside of the app.
*/
var isAuthenticating = false
/**
* Launches biometric prompt.
*
* @param title String title that will be shown on the prompt
* @param subtitle Optional string subtitle that will be shown on the prompt
* @param confirmationRequired Whether require explicit user confirmation after passive biometric is recognized
* @param callback Callback object to handle the authentication events
*/
fun FragmentActivity.startAuthentication(
title: String,
subtitle: String? = null,
confirmationRequired: Boolean = true,
callback: AuthenticationCallback
) {
isAuthenticating = true
val executor: Executor = ContextCompat.getMainExecutor(this)
val biometricPrompt = BiometricPrompt(
this,
executor,
callback
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK)
.build()
biometricPrompt.authenticate(promptInfo)
}
/**
* Returns true if Class 2 biometric or credential lock is set and available to use
*/
fun Context.isAuthenticationSupported(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.DEVICE_CREDENTIAL
return BiometricManager.from(this).canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
}
/**
* [AuthenticationCallback] with extra check
*
* @see isAuthenticating
*/
abstract class AuthenticationCallback : BiometricPrompt.AuthenticationCallback() {
@CallSuper
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
isAuthenticating = false
}
@CallSuper
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
isAuthenticating = false
}
@CallSuper
override fun onAuthenticationFailed() {
isAuthenticating = false
}
}
}