mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
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:
parent
e93e538600
commit
31fbb11c4a
6 changed files with 134 additions and 17 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue