diff --git a/app/build.gradle b/app/build.gradle
index f33a8bdbcc..27f478aa1d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -116,6 +116,11 @@ dependencies {
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.multidex:multidex:2.0.1'
+ implementation 'androidx.biometric:biometric:1.0.1'
+
+ final lifecycle_version = '2.1.0'
+ implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// UI library
implementation 'com.google.android.material:material:1.1.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 88dd239560..8a038bdd1b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,6 +54,9 @@
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt
index c3a338a457..fd68315bb4 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/App.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt
@@ -3,18 +3,26 @@ package eu.kanade.tachiyomi
import android.app.Application
import android.content.Context
import android.content.res.Configuration
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDex
import com.evernote.android.job.JobManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.updater.UpdaterJob
+import eu.kanade.tachiyomi.ui.security.BiometricUnlockDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
import org.acra.ACRA
import org.acra.annotation.ReportsCrashes
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
+import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar
@ReportsCrashes(
@@ -24,7 +32,7 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
buildConfigClass = BuildConfig::class,
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
)
-open class App : Application() {
+open class App : Application(), LifecycleObserver {
override fun onCreate() {
super.onCreate()
@@ -38,6 +46,8 @@ open class App : Application() {
setupNotificationChannels()
LocaleHelper.updateConfiguration(this, resources.configuration)
+
+ ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
override fun attachBaseContext(base: Context) {
@@ -50,6 +60,14 @@ open class App : Application() {
LocaleHelper.updateConfiguration(this, newConfig, true)
}
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ fun onAppBackgrounded() {
+ val preferences: PreferencesHelper by injectLazy()
+ if (preferences.lockAppAfter().getOrDefault() >= 0) {
+ BiometricUnlockDelegate.locked = true
+ }
+ }
+
protected open fun setupAcra() {
ACRA.init(this)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
index bb4efcd0f6..d714dbd4cd 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
@@ -105,6 +105,12 @@ object PreferenceKeys {
const val startScreen = "start_screen"
+ const val useBiometricLock = "use_biometric_lock"
+
+ const val lockAppAfter = "lock_app_after"
+
+ const val lastAppUnlock = "last_app_unlock"
+
const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
index 22a4a6b664..d4500176c8 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
@@ -52,6 +52,12 @@ class PreferencesHelper(val context: Context) {
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
+ fun useBiometricLock() = rxPrefs.getBoolean(Keys.useBiometricLock, false)
+
+ fun lockAppAfter() = rxPrefs.getInteger(Keys.lockAppAfter, 0)
+
+ fun lastAppUnlock() = rxPrefs.getLong(Keys.lastAppUnlock, 0)
+
fun clear() = prefs.edit().clear().apply()
fun themeMode() = rxPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt
index 7da27f88c4..2935982f10 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt
@@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatDelegate
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.ui.security.BiometricUnlockDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
@@ -46,4 +47,9 @@ abstract class BaseActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
}
+ override fun onResume() {
+ super.onResume()
+ BiometricUnlockDelegate.onResume(this)
+ }
+
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
index 2973872c99..6a0addc6a6 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
@@ -33,6 +33,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
+import eu.kanade.tachiyomi.ui.security.BiometricUnlockDelegate
import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.GLUtil
@@ -149,6 +150,11 @@ class ReaderActivity : BaseRxActivity() {
initializeMenu()
}
+ override fun onResume() {
+ super.onResume()
+ BiometricUnlockDelegate.onResume(this)
+ }
+
/**
* Called when the activity is destroyed. Cleans up the viewer, configuration and any view.
*/
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockActivity.kt
new file mode 100644
index 0000000000..fdfaad2b81
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockActivity.kt
@@ -0,0 +1,45 @@
+package eu.kanade.tachiyomi.ui.security
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.biometric.BiometricPrompt
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import uy.kohesive.injekt.injectLazy
+import java.util.Date
+import java.util.concurrent.Executors
+
+/**
+ * Blank activity with a BiometricPrompt.
+ */
+class BiometricUnlockActivity : AppCompatActivity() {
+
+ private val preferences: PreferencesHelper by injectLazy()
+ private val executor = Executors.newSingleThreadExecutor()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ finishAffinity()
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ BiometricUnlockDelegate.locked = false
+ preferences.lastAppUnlock().set(Date().time)
+ finish()
+ }
+ })
+
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(getString(R.string.unlock_library))
+ .setDeviceCredentialAllowed(true)
+ .build()
+
+ biometricPrompt.authenticate(promptInfo)
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockDelegate.kt
new file mode 100644
index 0000000000..ab684fa649
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/BiometricUnlockDelegate.kt
@@ -0,0 +1,36 @@
+package eu.kanade.tachiyomi.ui.security
+
+import android.content.Intent
+import androidx.biometric.BiometricManager
+import androidx.fragment.app.FragmentActivity
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import uy.kohesive.injekt.injectLazy
+import java.util.Date
+
+object BiometricUnlockDelegate {
+
+ private val preferences by injectLazy()
+
+ var locked: Boolean = true
+
+ fun onResume(activity: FragmentActivity) {
+ val lockApp = preferences.useBiometricLock().getOrDefault()
+ if (lockApp && BiometricManager.from(activity).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
+ if (isAppLocked()) {
+ val intent = Intent(activity, BiometricUnlockActivity::class.java)
+ activity.startActivity(intent)
+ activity.overridePendingTransition(0, 0)
+ }
+ } else if (lockApp) {
+ preferences.useBiometricLock().set(false)
+ }
+ }
+
+ private fun isAppLocked(): Boolean {
+ return locked &&
+ (preferences.lockAppAfter().getOrDefault() <= 0
+ || Date().time >= preferences.lastAppUnlock().getOrDefault() + 60 * 1000 * preferences.lockAppAfter().getOrDefault())
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
index 14f1d0eb9a..b82ead818c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting
import android.os.Build
+import androidx.biometric.BiometricManager
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault
@@ -115,6 +116,36 @@ class SettingsGeneralController : SettingsController() {
defaultValue = "1"
summary = "%s"
}
+
+ if (BiometricManager.from(context).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
+ preferenceCategory {
+ titleRes = R.string.pref_category_security
+
+ switchPreference {
+ key = Keys.useBiometricLock
+ titleRes = R.string.lock_with_biometrics
+ defaultValue = false
+ }
+ intListPreference {
+ key = Keys.lockAppAfter
+ titleRes = R.string.lock_when_idle
+ val values = arrayOf("0", "1", "2", "5", "10", "-1")
+ entries = values.mapNotNull {
+ when (it) {
+ "-1" -> context.getString(R.string.lock_never)
+ "0" -> context.getString(R.string.lock_always)
+ else -> resources?.getQuantityString(R.plurals.lock_after_mins, it.toInt(), it)
+ }
+ }.toTypedArray()
+ entryValues = values
+ defaultValue = "0"
+ summary = "%s"
+
+ preferences.useBiometricLock().asObservable()
+ .subscribeUntilDestroy { isVisible = it }
+ }
+ }
+ }
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ed53ea60e8..49a9e922e8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -24,6 +24,7 @@
Extension info
Help
+ Unlock Library
Settings
@@ -131,6 +132,16 @@
System default
Date format
+ Security
+ Lock with biometrics
+ Lock when idle
+ Always
+ Never
+
+ - After 1 minute
+ - After %1$s minutes
+
+
Display
Library manga per row